Browse Source

Development

master
Dwayne Harris 6 years ago
parent
commit
8eddb9d26c
  1. 8
      elpha-ios.xcodeproj/project.pbxproj
  2. 15
      elpha-ios/AccountTableViewController.swift
  3. 12
      elpha-ios/Assets.xcassets/Globe White.imageset/Contents.json
  4. 0
      elpha-ios/Assets.xcassets/Globe White.imageset/globe-white.pdf
  5. 2
      elpha-ios/Assets.xcassets/Globe.imageset/Contents.json
  6. BIN
      elpha-ios/Assets.xcassets/Globe.imageset/globe.pdf
  7. 12
      elpha-ios/Assets.xcassets/Home.imageset/Contents.json
  8. BIN
      elpha-ios/Assets.xcassets/Home.imageset/home.pdf
  9. 12
      elpha-ios/Assets.xcassets/Tag.imageset/Contents.json
  10. BIN
      elpha-ios/Assets.xcassets/Tag.imageset/tag.pdf
  11. 12
      elpha-ios/Assets.xcassets/Users.imageset/Contents.json
  12. BIN
      elpha-ios/Assets.xcassets/Users.imageset/users.pdf
  13. 307
      elpha-ios/AttachmentsManager.swift
  14. 31
      elpha-ios/AuthenticateViewController.swift
  15. 34
      elpha-ios/AuthenticationManager.swift
  16. 162
      elpha-ios/Base.lproj/Main.storyboard
  17. 5
      elpha-ios/Configuration.swift
  18. 8
      elpha-ios/CoreDataManager.swift
  19. 23
      elpha-ios/Elpha.xcdatamodeld/Elpha.xcdatamodel/contents
  20. 6
      elpha-ios/InstanceViewController.swift
  21. 24
      elpha-ios/InstancesDataManager.swift
  22. 134
      elpha-ios/InstancesTableViewController.swift
  23. 2
      elpha-ios/MainTabBarController.swift
  24. 292
      elpha-ios/MastodonDataManager.swift
  25. 94
      elpha-ios/StatusView.swift
  26. 334
      elpha-ios/TimelineTableViewController.swift
  27. 4
      elpha-ios/TimelinesNavigationController.swift
  28. 14
      elpha-ios/TimelinesTableViewCell.swift
  29. 127
      elpha-ios/TimelinesTableViewController.swift

8
elpha-ios.xcodeproj/project.pbxproj

@ -41,6 +41,8 @@
15960E84213774FC00C38CE9 /* InstancesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15960E83213774FC00C38CE9 /* InstancesTableViewController.swift */; };
15A79B2E215C63B6007A326E /* AlamofireImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1517EA842159D72200DE80D6 /* AlamofireImage.framework */; };
15A79B43215EB959007A326E /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A79B42215EB959007A326E /* CoreDataManager.swift */; };
15BB72A92171A6BE002F1FA4 /* TimelinesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15BB72A82171A6BE002F1FA4 /* TimelinesTableViewController.swift */; };
15BB72AB2171A8D4002F1FA4 /* TimelinesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15BB72AA2171A8D4002F1FA4 /* TimelinesTableViewCell.swift */; };
15C91A02216AB2D600D97DC3 /* NewStatusesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 15C91A01216AB2D600D97DC3 /* NewStatusesView.xib */; };
15C91A04216AB32500D97DC3 /* NewStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15C91A03216AB32500D97DC3 /* NewStatusesView.swift */; };
15F9981721629965009E58DA /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F9981621629965009E58DA /* TimelineTableViewController.swift */; };
@ -310,6 +312,8 @@
15A79AE7215B3CC5007A326E /* MastodonKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = MastodonKit.xcodeproj; path = Frameworks/MastodonKit/MastodonKit.xcodeproj; sourceTree = "<group>"; };
15A79B08215B438C007A326E /* OAuthSwift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OAuthSwift.xcodeproj; path = Frameworks/OAuthSwift/OAuthSwift.xcodeproj; sourceTree = "<group>"; };
15A79B42215EB959007A326E /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = "<group>"; };
15BB72A82171A6BE002F1FA4 /* TimelinesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesTableViewController.swift; sourceTree = "<group>"; };
15BB72AA2171A8D4002F1FA4 /* TimelinesTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesTableViewCell.swift; sourceTree = "<group>"; };
15C91A01216AB2D600D97DC3 /* NewStatusesView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NewStatusesView.xib; sourceTree = "<group>"; };
15C91A03216AB32500D97DC3 /* NewStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusesView.swift; sourceTree = "<group>"; };
15F9981621629965009E58DA /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
@ -382,6 +386,7 @@
isa = PBXGroup;
children = (
159048AE214F5015004F4014 /* InstancesTableViewCell.swift */,
15BB72AA2171A8D4002F1FA4 /* TimelinesTableViewCell.swift */,
159026AD2162CF5600D362DD /* TimelineTableViewCell.swift */,
);
name = Views;
@ -477,6 +482,7 @@
15960E792132387A00C38CE9 /* MainTabBarController.swift */,
15960E5E213145E100C38CE9 /* SecondViewController.swift */,
15960E812136668500C38CE9 /* TimelinesNavigationController.swift */,
15BB72A82171A6BE002F1FA4 /* TimelinesTableViewController.swift */,
15F9981621629965009E58DA /* TimelineTableViewController.swift */,
);
name = "View Controllers";
@ -803,6 +809,7 @@
15960E7E21329FED00C38CE9 /* AuthenticateViewController.swift in Sources */,
15960E5B213145E100C38CE9 /* AppDelegate.swift in Sources */,
15131EF2216D8D570092B252 /* StatusView.swift in Sources */,
15BB72A92171A6BE002F1FA4 /* TimelinesTableViewController.swift in Sources */,
15960E7721322C6F00C38CE9 /* Configuration.swift in Sources */,
15C91A04216AB32500D97DC3 /* NewStatusesView.swift in Sources */,
15131EF6216DBA820092B252 /* AccountNavigationController.swift in Sources */,
@ -811,6 +818,7 @@
15960E7321322BC700C38CE9 /* KeychainItemAccessibility.swift in Sources */,
159026D02163069600D362DD /* Date+TimeAgo.swift in Sources */,
15A79B43215EB959007A326E /* CoreDataManager.swift in Sources */,
15BB72AB2171A8D4002F1FA4 /* TimelinesTableViewCell.swift in Sources */,
1574148D2169AD0100C841BD /* AttachmentsManager.swift in Sources */,
15960E822136668500C38CE9 /* TimelinesNavigationController.swift in Sources */,
159026AE2162CF5600D362DD /* TimelineTableViewCell.swift in Sources */,

15
elpha-ios/AccountTableViewController.swift

@ -11,6 +11,7 @@ import MastodonKit
import UIKit
class AccountTableViewController: UITableViewController {
@IBOutlet var headerView: UIView!
@IBOutlet var headerImageView: UIImageView!
@IBOutlet var avatarImageView: UIImageView!
@IBOutlet var displayNameLabel: UILabel!
@ -22,7 +23,7 @@ class AccountTableViewController: UITableViewController {
public var account: AccountMO? = nil
private func setAccount(_ account: AccountMO) {
private func updateHeader(withAccount account: AccountMO) {
let avatarFilter = AspectScaledToFillSizeWithRoundedCornersFilter(
size: CGSize(width: 70.0, height: 70.0),
radius: 20.0,
@ -63,18 +64,14 @@ class AccountTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let navigationController = navigationController {
navigationController.view.backgroundColor = .clear
}
if self.account == nil {
if let session = AuthenticationManager.shared.selectedSession {
if let session = AuthenticationManager.shared.session {
self.account = session.account
}
}
if let account = account {
setAccount(account)
updateHeader(withAccount: account)
}
}
@ -82,7 +79,7 @@ class AccountTableViewController: UITableViewController {
super.viewDidAppear(animated)
if let account = self.account {
if let client = AuthenticationManager.shared.mkClientForSelectedSession() {
if let client = AuthenticationManager.shared.mkClient {
let request = Accounts.account(id: account.id!)
client.run(request) { result in
@ -92,7 +89,7 @@ class AccountTableViewController: UITableViewController {
if let account = self.account {
DispatchQueue.main.async {
self.setAccount(account)
self.updateHeader(withAccount: account)
}
}
case .failure(let error):

12
elpha-ios/Assets.xcassets/Globe White.imageset/Contents.json

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "globe-white.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

0
elpha-ios/Assets.xcassets/Globe.imageset/globe-white.pdf → elpha-ios/Assets.xcassets/Globe White.imageset/globe-white.pdf

2
elpha-ios/Assets.xcassets/Globe.imageset/Contents.json

@ -2,7 +2,7 @@
"images" : [
{
"idiom" : "universal",
"filename" : "globe-white.pdf"
"filename" : "globe.pdf"
}
],
"info" : {

BIN
elpha-ios/Assets.xcassets/Globe.imageset/globe.pdf

12
elpha-ios/Assets.xcassets/Home.imageset/Contents.json

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "home.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
elpha-ios/Assets.xcassets/Home.imageset/home.pdf

12
elpha-ios/Assets.xcassets/Tag.imageset/Contents.json

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "tag.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
elpha-ios/Assets.xcassets/Tag.imageset/tag.pdf

12
elpha-ios/Assets.xcassets/Users.imageset/Contents.json

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "users.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
elpha-ios/Assets.xcassets/Users.imageset/users.pdf

307
elpha-ios/AttachmentsManager.swift

@ -39,241 +39,126 @@ class AttachmentsManager {
)
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraint(getLeadingConstraint(from: imageView, to: view))
view.addConstraint(getTrailingConstraint(from: imageView, to: view))
view.addConstraint(getTopConstraint(from: imageView, to: view))
view.addConstraint(getBottomConstraint(from: imageView, to: view))
imageView.addConstraint(getWidthConstraint(view: imageView, width: view.frame.width))
imageView.addConstraint(getHeightConstraint(view: imageView, height: view.frame.width))
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.topAnchor),
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.widthAnchor.constraint(equalToConstant: view.frame.width),
imageView.heightAnchor.constraint(equalToConstant: view.frame.width),
])
case 2:
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: halfWidth, height: view.frame.width))
let firstImageView = UIImageView()
let secondImageView = UIImageView()
let imageViews = [
UIImageView(),
UIImageView(),
]
[firstImageView, secondImageView].forEach { imageView in
for (index, imageView) in imageViews.enumerated() {
imageView.contentMode = UIImageView.ContentMode.scaleAspectFill
imageView.af_setImage(
withURL: (attachments[index] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.topAnchor),
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.widthAnchor.constraint(equalToConstant: halfWidth),
imageView.heightAnchor.constraint(equalToConstant: view.frame.width),
])
}
firstImageView.af_setImage(
withURL: (attachments[0] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
secondImageView.af_setImage(
withURL: (attachments[1] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
view.addSubview(firstImageView)
view.addSubview(secondImageView)
firstImageView.translatesAutoresizingMaskIntoConstraints = false
secondImageView.translatesAutoresizingMaskIntoConstraints = false
[firstImageView, secondImageView].forEach { imageView in
view.addConstraint(getLeadingConstraint(from: imageView, to: view))
view.addConstraint(getTrailingConstraint(from: imageView, to: view))
view.addConstraint(getTopConstraint(from: imageView, to: view))
view.addConstraint(getBottomConstraint(from: imageView, to: view))
imageView.addConstraint(getHeightConstraint(view: imageView, height: view.frame.width))
imageView.addConstraint(getWidthConstraint(view: imageView, width: halfWidth))
}
NSLayoutConstraint.activate([
imageViews[0].leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageViews[1].trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
case 3:
let primaryFilter = AspectScaledToFillSizeFilter(size: CGSize(width: halfWidth, height: view.frame.width))
let secondaryFilter = AspectScaledToFillSizeFilter(size: CGSize(width: halfWidth, height: halfWidth))
let firstImageView = UIImageView()
let secondImageView = UIImageView()
let thirdImageView = UIImageView()
let imageViews = [
UIImageView(),
UIImageView(),
UIImageView(),
]
[firstImageView, secondImageView, thirdImageView].forEach { imageView in
for (index, imageView) in imageViews.enumerated() {
imageView.contentMode = UIImageView.ContentMode.scaleAspectFill
imageView.af_setImage(
withURL: (attachments[index] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: index == 0 ? primaryFilter : secondaryFilter
)
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
if index == 0 {
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageView.topAnchor.constraint(equalTo: view.topAnchor),
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.widthAnchor.constraint(equalToConstant: halfWidth),
imageView.heightAnchor.constraint(equalToConstant: view.frame.width),
])
} else {
NSLayoutConstraint.activate([
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.widthAnchor.constraint(equalToConstant: halfWidth),
imageView.heightAnchor.constraint(equalToConstant: halfWidth),
])
}
}
firstImageView.af_setImage(
withURL: (attachments[0] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: primaryFilter
)
secondImageView.af_setImage(
withURL: (attachments[1] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: secondaryFilter
)
thirdImageView.af_setImage(
withURL: (attachments[2] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: secondaryFilter
)
view.addSubview(firstImageView)
view.addSubview(secondImageView)
view.addSubview(thirdImageView)
firstImageView.translatesAutoresizingMaskIntoConstraints = false
secondImageView.translatesAutoresizingMaskIntoConstraints = false
thirdImageView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraint(getLeadingConstraint(from: firstImageView, to: view))
view.addConstraint(getTopConstraint(from: firstImageView, to: view))
view.addConstraint(getBottomConstraint(from: firstImageView, to: view))
firstImageView.addConstraint(getHeightConstraint(view: firstImageView, height: view.frame.width))
firstImageView.addConstraint(getWidthConstraint(view: firstImageView, width: halfWidth))
view.addConstraint(getTopConstraint(from: secondImageView, to: view))
view.addConstraint(getBottomConstraint(from: thirdImageView, to: view))
[secondImageView, thirdImageView].forEach { imageView in
view.addConstraint(getTrailingConstraint(from: imageView, to: view))
imageView.addConstraint(getHeightConstraint(view: imageView, height: halfWidth))
imageView.addConstraint(getWidthConstraint(view: imageView, width: halfWidth))
}
NSLayoutConstraint.activate([
imageViews[2].topAnchor.constraint(equalTo: view.topAnchor),
imageViews[3].bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
default:
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: halfWidth, height: halfWidth))
let firstImageView = UIImageView()
let secondImageView = UIImageView()
let thirdImageView = UIImageView()
let fourthImageView = UIImageView()
let imageViews = [
UIImageView(),
UIImageView(),
UIImageView(),
UIImageView(),
]
[firstImageView, secondImageView, thirdImageView, fourthImageView].forEach { imageView in
for (index, imageView) in imageViews.enumerated() {
imageView.contentMode = UIImageView.ContentMode.scaleAspectFill
imageView.af_setImage(
withURL: (attachments[index] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: halfWidth),
imageView.heightAnchor.constraint(equalToConstant: halfWidth),
])
}
firstImageView.af_setImage(
withURL: (attachments[0] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
secondImageView.af_setImage(
withURL: (attachments[1] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
thirdImageView.af_setImage(
withURL: (attachments[2] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
fourthImageView.af_setImage(
withURL: (attachments[3] as! AttachmentMO).url!,
placeholderImage: placeholderImage,
filter: filter
)
view.addSubview(firstImageView)
view.addSubview(secondImageView)
view.addSubview(thirdImageView)
view.addSubview(fourthImageView)
firstImageView.translatesAutoresizingMaskIntoConstraints = false
secondImageView.translatesAutoresizingMaskIntoConstraints = false
thirdImageView.translatesAutoresizingMaskIntoConstraints = false
fourthImageView.translatesAutoresizingMaskIntoConstraints = false
[firstImageView, secondImageView, thirdImageView, fourthImageView].forEach { imageView in
imageView.addConstraint(getHeightConstraint(view: imageView, height: halfWidth))
imageView.addConstraint(getWidthConstraint(view: imageView, width: halfWidth))
}
[firstImageView, secondImageView].forEach { imageView in
view.addConstraint(getTopConstraint(from: imageView, to: view))
}
[thirdImageView, fourthImageView].forEach { imageView in
view.addConstraint(getBottomConstraint(from: imageView, to: view))
}
[firstImageView, thirdImageView].forEach { imageView in
view.addConstraint(getLeadingConstraint(from: imageView, to: view))
}
[secondImageView, fourthImageView].forEach { imageView in
view.addConstraint(getTrailingConstraint(from: imageView, to: view))
}
NSLayoutConstraint.activate([
imageViews[0].topAnchor.constraint(equalTo: view.topAnchor),
imageViews[0].leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageViews[1].topAnchor.constraint(equalTo: view.topAnchor),
imageViews[1].trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageViews[3].leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageViews[3].bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageViews[4].bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageViews[4].trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
}
static func getTopConstraint(from: UIView, to: UIView) -> NSLayoutConstraint {
return NSLayoutConstraint(
item: from,
attribute: NSLayoutConstraint.Attribute.top,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: to,
attribute: NSLayoutConstraint.Attribute.top,
multiplier: 1,
constant: 0
)
}
static func getBottomConstraint(from: UIView, to: UIView) -> NSLayoutConstraint {
return NSLayoutConstraint(
item: from,
attribute: NSLayoutConstraint.Attribute.bottom,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: to,
attribute: NSLayoutConstraint.Attribute.bottom,
multiplier: 1,
constant: 0
)
}
static func getLeadingConstraint(from: UIView, to: UIView) -> NSLayoutConstraint {
return NSLayoutConstraint(
item: from,
attribute: NSLayoutConstraint.Attribute.leading,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: to,
attribute: NSLayoutConstraint.Attribute.leading,
multiplier: 1,
constant: 0
)
}
static func getTrailingConstraint(from: UIView, to: UIView) -> NSLayoutConstraint {
return NSLayoutConstraint(
item: from,
attribute: NSLayoutConstraint.Attribute.trailing,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: to,
attribute: NSLayoutConstraint.Attribute.trailing,
multiplier: 1,
constant: 0
)
}
static func getWidthConstraint(view: UIView, width: CGFloat) -> NSLayoutConstraint {
return NSLayoutConstraint(
item: view,
attribute: NSLayoutConstraint.Attribute.width,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: nil,
attribute: NSLayoutConstraint.Attribute.notAnAttribute,
multiplier: 1,
constant: width
)
}
static func getHeightConstraint(view: UIView, height: CGFloat) -> NSLayoutConstraint {
return NSLayoutConstraint(
item: view,
attribute: NSLayoutConstraint.Attribute.height,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: nil,
attribute: NSLayoutConstraint.Attribute.notAnAttribute,
multiplier: 1,
constant: height
)
}
}

31
elpha-ios/AuthenticateViewController.swift

@ -6,6 +6,7 @@
// Copyright © 2018 Elpha. All rights reserved.
//
import Alamofire
import CoreData
import MastodonKit
import OAuthSwift
@ -16,18 +17,46 @@ class AuthenticateViewController: UIViewController {
@IBOutlet var instanceTextField: UITextField!
var oauthswift: OAuthSwift?
let defaultInstanceName = "mastodon.social"
override func viewDidLoad() {
super.viewDidLoad()
signInButton.layer.cornerRadius = 10
signInButton.clipsToBounds = true
randomInstanceName { instance in
self.instanceTextField.attributedPlaceholder = NSAttributedString(string: instance, attributes: [NSAttributedString.Key.foregroundColor: UIColor.init(red: 0.9, green: 0.9, blue: 0.9, alpha: 1)])
}
}
override open var shouldAutorotate: Bool {
return false
}
func randomInstanceName(completion: @escaping (String) -> Void) {
let requestURL = "\(Config.instancesServiceURL)\(Config.instancesServiceRandomEndpoint)?count=1"
let headers: HTTPHeaders = ["Authorization": "Bearer \(Config.instancesServiceSecret)"]
Alamofire.request(requestURL, headers: headers).validate().responseJSON { response in
switch response.result {
case .success(let value):
guard let result = value as? [String: Any],
let instances = result["instances"] as? [Any],
let instance = instances.first as? [String: Any],
let name = instance["name"] as? String else {
completion(self.defaultInstanceName)
return
}
completion(name)
case .failure(let error):
print("\(error)")
completion(self.defaultInstanceName)
}
}
}
func authorize(client: ClientMO) {
let oauthswift = OAuth2Swift(
consumerKey: client.clientID!,
@ -95,7 +124,7 @@ class AuthenticateViewController: UIViewController {
request.predicate = NSPredicate(format: "url == %@", url)
do {
let response = try CoreDataManager.shared.getContext().fetch(request)
let response = try CoreDataManager.shared.context.fetch(request)
if let client = response.first {
self.authorize(client: client)
} else {

34
elpha-ios/AuthenticationManager.swift

@ -13,25 +13,25 @@ import UIKit
class AuthenticationManager {
static let shared = AuthenticationManager()
var sessionCount: Int {
var sessions: [SessionMO] {
get {
do {
let request = NSFetchRequest<SessionMO>(entityName: "Session")
return try CoreDataManager.shared.getContext().count(for: request)
return try CoreDataManager.shared.context.fetch(request)
} catch {
print("Error counting Sessions \(error)")
return 0
print("Error fetching Sessions \(error)")
return []
}
}
}
var selectedSession: SessionMO? {
var session: SessionMO? {
get {
let request = NSFetchRequest<SessionMO>(entityName: "Session")
request.predicate = NSPredicate(format: "selected = YES")
do {
let results = try CoreDataManager.shared.getContext().fetch(request)
let results = try CoreDataManager.shared.context.fetch(request)
return results.first
} catch {
print("Error fetching Session \(error)")
@ -40,9 +40,9 @@ class AuthenticationManager {
}
}
var selectedSessionToken: String? {
var token: String? {
get {
guard let session = selectedSession, let account = session.account else {
guard let account = session?.account else {
return nil
}
@ -50,20 +50,22 @@ class AuthenticationManager {
}
}
func mkClientForSelectedSession() -> Client? {
guard let session = selectedSession, let client = session.client else {
return nil
var mkClient: Client? {
get {
guard let client = session?.client else {
return nil
}
return Client(baseURL: "https://\(client.url!)", accessToken: token)
}
return Client(baseURL: "https://\(client.url!)", accessToken: selectedSessionToken)
}
func saveSession(client: ClientMO, account: AccountMO, token: String) -> SessionMO? {
if let selectedSession = selectedSession {
selectedSession.selected = false
if let session = self.session {
session.selected = false
}
let session = SessionMO(context: CoreDataManager.shared.getContext())
let session = SessionMO(context: CoreDataManager.shared.context)
session.client = client
session.account = account
session.order = 0

162
elpha-ios/Base.lproj/Main.storyboard

@ -23,7 +23,7 @@
<color key="textColor" red="0.54117647058823526" green="0.4823529411764706" blue="0.68235294117647061" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="mastodon.social" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="02G-kI-p4F">
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="02G-kI-p4F">
<rect key="frame" x="10" y="458" width="394" height="40"/>
<color key="backgroundColor" red="0.54117647058823526" green="0.4823529411764706" blue="0.68235294117647061" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
@ -31,7 +31,7 @@
</constraints>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<textInputTraits key="textInputTraits"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="URL" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="awW-03-3TB">
<rect key="frame" x="80" y="518" width="254" height="40"/>
@ -143,7 +143,7 @@
<constraint firstAttribute="height" constant="30" id="v20-Wv-cjD"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="EuF-dW-Uua">
@ -155,13 +155,13 @@
<constraint firstAttribute="height" constant="16" id="aZP-9T-7kG"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.66422420739999999" green="0.66424006219999998" blue="0.66423153879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4n9-eS-zCc">
<rect key="frame" x="0.0" y="29" width="53" height="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<nil key="textColor"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
@ -185,13 +185,13 @@
<constraint firstAttribute="height" constant="16" id="cyk-90-Tuq"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.66422420739999999" green="0.66424006219999998" blue="0.66423153879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4fE-NE-pXx">
<rect key="frame" x="0.0" y="29" width="37.5" height="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<nil key="textColor"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
@ -238,8 +238,16 @@
<refreshControl key="refreshControl" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="agk-Mw-NTN">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
<attributedString key="attributedTitle"/>
<color key="tintColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<attributedString key="attributedTitle">
<fragment content="Loading Instances ...">
<attributes>
<color key="NSColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<font key="NSFont" size="12" name=".SFNSText"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
</attributes>
</fragment>
</attributedString>
<color key="tintColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</refreshControl>
<connections>
<outlet property="mainNavigationItem" destination="mfC-MW-ufJ" id="YZK-rX-Gn3"/>
@ -272,7 +280,7 @@
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ztD-p8-9YN">
<rect key="frame" x="8" y="210" width="398" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" red="0.66422420739999999" green="0.66424006219999998" blue="0.66423153879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Eld-tn-44S">
@ -298,7 +306,7 @@
<constraint firstAttribute="height" constant="16" id="BOa-EW-chw"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.66422420739999999" green="0.66424006219999998" blue="0.66423153879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ad5-DU-QLI">
@ -330,7 +338,7 @@
<constraint firstAttribute="height" constant="16" id="ttv-xx-js7"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.66422420739999999" green="0.66424006219999998" blue="0.66423153879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="npd-Y6-d6W">
@ -362,7 +370,7 @@
<constraint firstAttribute="height" constant="16" id="HIC-Cl-aHL"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.66422420739999999" green="0.66424006219999998" blue="0.66423153879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Rfc-EP-QB2">
@ -394,7 +402,7 @@
<constraint firstAttribute="height" constant="16" id="pdS-qH-b3B"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.66422420739999999" green="0.66424006219999998" blue="0.66423153879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SL3-SA-8IS">
@ -481,36 +489,36 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="ygB-F6-Moa">
<rect key="frame" x="0.0" y="0.0" width="414" height="408"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="489"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0jO-Rb-lcz">
<rect key="frame" x="0.0" y="-2" width="414" height="200"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="200"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="HWQ-9W-TLG"/>
</constraints>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="d7X-u9-Seq">
<rect key="frame" x="172" y="163" width="70" height="70"/>
<rect key="frame" x="172" y="165" width="70" height="70"/>
<constraints>
<constraint firstAttribute="height" constant="70" id="6Ni-kg-YNd"/>
<constraint firstAttribute="width" constant="70" id="ItK-PQ-Zdt"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MGa-Tw-aUj">
<rect key="frame" x="142.5" y="248" width="129.5" height="24"/>
<rect key="frame" x="142.5" y="250" width="129.5" height="24"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XET-LP-bGO">
<rect key="frame" x="174" y="277" width="66" height="17"/>
<rect key="frame" x="174" y="279" width="66" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="d8Z-vZ-agH">
<rect key="frame" x="20" y="309" width="374" height="50"/>
<rect key="frame" x="20" y="311" width="374" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="a6x-gd-oBT"/>
</constraints>
@ -519,16 +527,16 @@
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="D5v-MR-CuQ">
<rect key="frame" x="20" y="374" width="100" height="14"/>
<rect key="frame" x="20" y="376" width="118" height="65"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Toots" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kTz-EN-Zgx">
<rect key="frame" x="8" y="10" width="84" height="14.5"/>
<rect key="frame" x="8" y="10" width="102" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Eab-8s-Drh">
<rect key="frame" x="8" y="32.5" width="84" height="24"/>
<rect key="frame" x="8" y="32.5" width="102" height="24"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@ -540,22 +548,21 @@
<constraint firstAttribute="trailing" secondItem="kTz-EN-Zgx" secondAttribute="trailing" constant="8" id="6Dd-wW-L1W"/>
<constraint firstItem="Eab-8s-Drh" firstAttribute="top" secondItem="kTz-EN-Zgx" secondAttribute="bottom" constant="8" id="Aiy-x2-NuW"/>
<constraint firstItem="Eab-8s-Drh" firstAttribute="leading" secondItem="D5v-MR-CuQ" secondAttribute="leading" constant="8" id="Yiw-Ui-WMd"/>
<constraint firstAttribute="width" constant="100" id="b2A-8e-vQr"/>
<constraint firstItem="kTz-EN-Zgx" firstAttribute="leading" secondItem="D5v-MR-CuQ" secondAttribute="leading" constant="8" id="dkF-ab-7du"/>
<constraint firstItem="kTz-EN-Zgx" firstAttribute="top" secondItem="D5v-MR-CuQ" secondAttribute="top" constant="10" id="mLg-bw-Gqg"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TkS-vn-sW3">
<rect key="frame" x="157" y="374" width="100" height="14"/>
<rect key="frame" x="148" y="376" width="118" height="65"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Following" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FxQ-nq-jYw">
<rect key="frame" x="8" y="10" width="84" height="14.5"/>
<rect key="frame" x="8" y="10" width="102" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l3U-wO-M4g">
<rect key="frame" x="8" y="32.5" width="84" height="24"/>
<rect key="frame" x="8" y="32.5" width="102" height="24"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@ -563,7 +570,6 @@
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="7Uy-eK-pPB"/>
<constraint firstItem="FxQ-nq-jYw" firstAttribute="top" secondItem="TkS-vn-sW3" secondAttribute="top" constant="10" id="8nY-kC-dnK"/>
<constraint firstAttribute="trailing" secondItem="l3U-wO-M4g" secondAttribute="trailing" constant="8" id="9yP-IG-4TK"/>
<constraint firstItem="l3U-wO-M4g" firstAttribute="top" secondItem="FxQ-nq-jYw" secondAttribute="bottom" constant="8" id="RRv-UW-tu8"/>
@ -573,16 +579,16 @@
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rqF-sG-ZE7">
<rect key="frame" x="294" y="374" width="100" height="14"/>
<rect key="frame" x="276" y="376" width="118" height="65"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Followers" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="piT-tX-tUe">
<rect key="frame" x="8" y="10" width="84" height="14.5"/>
<rect key="frame" x="8" y="10" width="102" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="J8H-VZ-dwT">
<rect key="frame" x="8" y="32.5" width="84" height="24"/>
<rect key="frame" x="8" y="32.5" width="102" height="24"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@ -594,40 +600,55 @@
<constraint firstAttribute="trailing" secondItem="piT-tX-tUe" secondAttribute="trailing" constant="8" id="2La-A7-kHB"/>
<constraint firstItem="piT-tX-tUe" firstAttribute="leading" secondItem="rqF-sG-ZE7" secondAttribute="leading" constant="8" id="2cc-DJ-nQb"/>
<constraint firstItem="J8H-VZ-dwT" firstAttribute="leading" secondItem="rqF-sG-ZE7" secondAttribute="leading" constant="8" id="BGa-ZO-3IO"/>
<constraint firstAttribute="width" constant="100" id="D8N-BK-ZSH"/>
<constraint firstItem="J8H-VZ-dwT" firstAttribute="top" secondItem="piT-tX-tUe" secondAttribute="bottom" constant="8" id="Zdh-yX-wjL"/>
<constraint firstItem="piT-tX-tUe" firstAttribute="top" secondItem="rqF-sG-ZE7" secondAttribute="top" constant="10" id="h9M-Od-Xeu"/>
</constraints>
</view>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="lwU-IL-uAN">
<rect key="frame" x="10" y="451" width="394" height="29"/>
<segments>
<segment title="Toots"/>
<segment title="Toots &amp; Replies"/>
<segment title="Media"/>
</segments>
<color key="tintColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0jO-Rb-lcz" firstAttribute="leading" secondItem="ygB-F6-Moa" secondAttribute="leading" id="0Od-Hb-lt5"/>
<constraint firstItem="TkS-vn-sW3" firstAttribute="width" secondItem="rqF-sG-ZE7" secondAttribute="width" id="0rh-o4-LVs"/>
<constraint firstAttribute="trailing" secondItem="lwU-IL-uAN" secondAttribute="trailing" constant="10" id="1F8-BY-pk1"/>
<constraint firstItem="TkS-vn-sW3" firstAttribute="top" secondItem="d8Z-vZ-agH" secondAttribute="bottom" constant="15" id="1Nv-md-bcK"/>
<constraint firstItem="rqF-sG-ZE7" firstAttribute="leading" secondItem="TkS-vn-sW3" secondAttribute="trailing" constant="10" id="5AH-V8-cCY"/>
<constraint firstItem="XET-LP-bGO" firstAttribute="centerX" secondItem="ygB-F6-Moa" secondAttribute="centerX" id="8q3-M6-fle"/>
<constraint firstAttribute="bottom" secondItem="rqF-sG-ZE7" secondAttribute="bottom" constant="20" id="J5e-cr-T32"/>
<constraint firstItem="MGa-Tw-aUj" firstAttribute="centerX" secondItem="ygB-F6-Moa" secondAttribute="centerX" id="Ka8-Yt-VNQ"/>
<constraint firstItem="D5v-MR-CuQ" firstAttribute="leading" secondItem="ygB-F6-Moa" secondAttribute="leading" constant="20" id="Sfb-al-bD3"/>
<constraint firstItem="d8Z-vZ-agH" firstAttribute="top" secondItem="XET-LP-bGO" secondAttribute="bottom" constant="15" id="VdD-Ow-Hbq"/>
<constraint firstItem="0jO-Rb-lcz" firstAttribute="top" secondItem="ygB-F6-Moa" secondAttribute="top" constant="-2" id="avA-XU-Hlb"/>
<constraint firstItem="lwU-IL-uAN" firstAttribute="top" secondItem="rqF-sG-ZE7" secondAttribute="bottom" constant="10" id="WTw-4o-oN0"/>
<constraint firstItem="D5v-MR-CuQ" firstAttribute="width" secondItem="TkS-vn-sW3" secondAttribute="width" id="Wh6-R2-fsh"/>
<constraint firstAttribute="bottom" secondItem="lwU-IL-uAN" secondAttribute="bottom" constant="10" id="YQf-Cp-ypH"/>
<constraint firstItem="0jO-Rb-lcz" firstAttribute="top" secondItem="ygB-F6-Moa" secondAttribute="top" id="avA-XU-Hlb"/>
<constraint firstItem="lwU-IL-uAN" firstAttribute="leading" secondItem="ygB-F6-Moa" secondAttribute="leading" constant="10" id="dkb-zx-jFp"/>
<constraint firstItem="d7X-u9-Seq" firstAttribute="centerX" secondItem="ygB-F6-Moa" secondAttribute="centerX" id="e6Y-Fv-aMf"/>
<constraint firstItem="d8Z-vZ-agH" firstAttribute="leading" secondItem="ygB-F6-Moa" secondAttribute="leading" constant="20" id="ePh-7B-g27"/>
<constraint firstItem="d7X-u9-Seq" firstAttribute="top" secondItem="0jO-Rb-lcz" secondAttribute="bottom" constant="-35" id="f0s-Vv-mmG"/>
<constraint firstAttribute="bottom" secondItem="D5v-MR-CuQ" secondAttribute="bottom" constant="20" id="ggl-n0-CmE"/>
<constraint firstItem="TkS-vn-sW3" firstAttribute="centerX" secondItem="ygB-F6-Moa" secondAttribute="centerX" id="h25-tL-Eou"/>
<constraint firstItem="XET-LP-bGO" firstAttribute="top" secondItem="MGa-Tw-aUj" secondAttribute="bottom" constant="5" id="h2h-6e-Jzt"/>
<constraint firstAttribute="trailing" secondItem="d8Z-vZ-agH" secondAttribute="trailing" constant="20" id="iaP-Eh-O8n"/>
<constraint firstItem="lwU-IL-uAN" firstAttribute="top" secondItem="TkS-vn-sW3" secondAttribute="bottom" constant="10" id="k5W-UT-Xho"/>
<constraint firstAttribute="trailing" secondItem="rqF-sG-ZE7" secondAttribute="trailing" constant="20" id="kPD-YC-0bD"/>
<constraint firstItem="D5v-MR-CuQ" firstAttribute="top" secondItem="d8Z-vZ-agH" secondAttribute="bottom" constant="15" id="nm6-TP-Qzj"/>
<constraint firstItem="MGa-Tw-aUj" firstAttribute="top" secondItem="d7X-u9-Seq" secondAttribute="bottom" constant="15" id="o9i-Ys-iwf"/>
<constraint firstAttribute="trailing" secondItem="0jO-Rb-lcz" secondAttribute="trailing" id="qGc-XW-65f"/>
<constraint firstAttribute="bottom" secondItem="TkS-vn-sW3" secondAttribute="bottom" constant="20" id="twi-lv-FYu"/>
<constraint firstItem="lwU-IL-uAN" firstAttribute="top" secondItem="D5v-MR-CuQ" secondAttribute="bottom" constant="10" id="qbc-oW-qvh"/>
<constraint firstItem="rqF-sG-ZE7" firstAttribute="top" secondItem="d8Z-vZ-agH" secondAttribute="bottom" constant="15" id="v9w-nR-6pE"/>
<constraint firstItem="TkS-vn-sW3" firstAttribute="leading" secondItem="D5v-MR-CuQ" secondAttribute="trailing" constant="10" id="wje-Vh-Xte"/>
</constraints>
</view>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AccountTableViewCell" id="nLQ-4Q-SwI">
<rect key="frame" x="0.0" y="436" width="414" height="44"/>
<rect key="frame" x="0.0" y="517" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nLQ-4Q-SwI" id="y0d-Ka-W2g">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -648,13 +669,14 @@
<outlet property="followersLabel" destination="J8H-VZ-dwT" id="XiN-FS-Wjw"/>
<outlet property="followingLabel" destination="l3U-wO-M4g" id="N5A-sA-0O1"/>
<outlet property="headerImageView" destination="0jO-Rb-lcz" id="30G-8E-1xD"/>
<outlet property="headerView" destination="ygB-F6-Moa" id="wXs-JO-7oD"/>
<outlet property="statusesLabel" destination="Eab-8s-Drh" id="zTr-ZY-uHR"/>
<outlet property="usernameLabel" destination="XET-LP-bGO" id="ffw-On-bkz"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dWu-5e-ce7" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1635" y="360"/>
<point key="canvasLocation" x="1634.7826086956522" y="359.59821428571428"/>
</scene>
<!--Main Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP">
@ -704,6 +726,63 @@
</objects>
<point key="canvasLocation" x="749.60000000000002" y="-320.68965517241384"/>
</scene>
<!--Timelines-->
<scene sceneID="rPf-bh-vX0">
<objects>
<tableViewController title="Timelines" definesPresentationContext="YES" providesPresentationContextTransitionStyle="YES" modalPresentationStyle="overCurrentContext" id="ohq-uu-dqB" customClass="TimelinesTableViewController" customModule="elpha_ios" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="au7-dg-KQQ">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" showsReorderControl="YES" indentationWidth="10" reuseIdentifier="TimelinesTableViewCell" rowHeight="100" id="HWA-v6-ifS" customClass="TimelinesTableViewCell" customModule="elpha_ios" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="414" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="HWA-v6-ifS" id="Xod-r8-3b7">
<rect key="frame" x="0.0" y="0.0" width="414" height="99.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="pn3-vi-rIP">
<rect key="frame" x="15" y="25" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="NVy-av-IC4"/>
<constraint firstAttribute="height" constant="50" id="w00-gF-A5N"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Timeline" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="65I-lo-p71">
<rect key="frame" x="73" y="39.5" width="313" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="pn3-vi-rIP" firstAttribute="leading" secondItem="Xod-r8-3b7" secondAttribute="leading" constant="15" id="3zj-oF-aEo"/>
<constraint firstItem="65I-lo-p71" firstAttribute="leading" secondItem="pn3-vi-rIP" secondAttribute="trailing" constant="8" id="UDs-ko-mUR"/>
<constraint firstItem="pn3-vi-rIP" firstAttribute="centerY" secondItem="Xod-r8-3b7" secondAttribute="centerY" id="adh-gg-Ujg"/>
<constraint firstItem="65I-lo-p71" firstAttribute="centerY" secondItem="Xod-r8-3b7" secondAttribute="centerY" id="faF-zM-PbW"/>
<constraint firstAttribute="trailingMargin" secondItem="65I-lo-p71" secondAttribute="trailing" constant="8" id="oBY-be-iTb"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="timelineImageView" destination="pn3-vi-rIP" id="9WN-FV-oBS"/>
<outlet property="timelineLabel" destination="65I-lo-p71" id="AOo-jV-DIO"/>
</connections>
</tableViewCell>
</prototypes>
<sections/>
<connections>
<outlet property="dataSource" destination="ohq-uu-dqB" id="G6S-L6-k4A"/>
<outlet property="delegate" destination="ohq-uu-dqB" id="rXh-e9-NRO"/>
</connections>
</tableView>
<value key="contentSizeForViewInPopover" type="size" width="300" height="300"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="e9T-Uk-D5X" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1634.7826086956522" y="-1043.3035714285713"/>
</scene>
<!--Timeline-->
<scene sceneID="jGL-v8-K0I">
<objects>
@ -748,6 +827,9 @@
<autoresizingMask key="autoresizingMask"/>
<attributedString key="attributedTitle"/>
</refreshControl>
<connections>
<segue destination="ohq-uu-dqB" kind="presentation" identifier="TimelinesSegue" modalPresentationStyle="overCurrentContext" id="lfm-EQ-h2m"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="KTy-Tn-EdD" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
@ -802,7 +884,7 @@
</scenes>
<resources>
<image name="Account" width="25" height="25"/>
<image name="Globe" width="25" height="25"/>
<image name="Globe" width="22" height="22"/>
<image name="Instance Placeholder" width="135" height="135"/>
<image name="Logo" width="400" height="400"/>
<image name="Timelines" width="25" height="25"/>

5
elpha-ios/Configuration.swift

@ -13,8 +13,9 @@ struct Config {
static let clientDisplayName = "Elpha"
static let clientWebsite = "https://elpha.xyz"
static let instancesServiceUrl = "https://instances.social"
static let instancesServiceEndpoint = "/api/1.0/instances/list"
static let instancesServiceURL = "https://instances.social"
static let instancesServiceListEndpoint = "/api/1.0/instances/list"
static let instancesServiceRandomEndpoint = "/api/1.0/instances/sample"
static let instancesServiceApplicationID = "104969750"
static let instancesServiceSecret = "xEGMUQL9w2tYU7CnkBMY555nmLYL1dryCdQNFZWQlwHqDg0cRPxF5nXx34LVM5Zt8CbvmNnj89nruMYe0jCzlky0lolAcoLOcNrNerh9m30Q85WskiOqEkHp0M5HhEeS"
}

8
elpha-ios/CoreDataManager.swift

@ -24,13 +24,13 @@ public class CoreDataManager {
return container
}()
func getContext() -> NSManagedObjectContext {
return persistentContainer.viewContext
var context: NSManagedObjectContext {
get {
return persistentContainer.viewContext
}
}
func saveContext() {
let context = getContext()
if context.hasChanges {
do {
try context.save()

23
elpha-ios/Elpha.xcdatamodeld/Elpha.xcdatamodel/contents

@ -43,6 +43,12 @@
<attribute name="url" attributeType="String" syncable="YES"/>
<relationship name="sessions" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Session" inverseName="client" inverseEntity="Session" syncable="YES"/>
</entity>
<entity name="Emoji" representedClassName="EmojiMO" syncable="YES" codeGenerationType="class">
<attribute name="shortcode" attributeType="String" syncable="YES"/>
<attribute name="staticURL" attributeType="URI" syncable="YES"/>
<attribute name="url" attributeType="URI" syncable="YES"/>
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
</entity>
<entity name="ISCategory" representedClassName="ISCategoryMO" syncable="YES" codeGenerationType="class">
<attribute name="id" attributeType="String" syncable="YES"/>
<relationship name="instances" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ISInstance" inverseName="categories" inverseEntity="ISInstance" syncable="YES"/>
@ -100,10 +106,10 @@
<attribute name="color" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="selected" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="selected" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="sessions" inverseEntity="Account" syncable="YES"/>
<relationship name="client" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Client" inverseName="sessions" inverseEntity="Client" syncable="YES"/>
<relationship name="selectedTimeline" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Timeline" inverseName="session" inverseEntity="Timeline" syncable="YES"/>
<relationship name="timeline" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Timeline" inverseName="session" inverseEntity="Timeline" syncable="YES"/>
</entity>
<entity name="Status" representedClassName="StatusMO" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
@ -124,6 +130,7 @@
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="statuses" inverseEntity="App" syncable="YES"/>
<relationship name="attachments" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="Attachment" inverseName="status" inverseEntity="Attachment" syncable="YES"/>
<relationship name="boundaries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimelineBoundary" inverseName="status" inverseEntity="TimelineBoundary" syncable="YES"/>
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="status" inverseEntity="Mention" syncable="YES"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogs" inverseEntity="Status" syncable="YES"/>
<relationship name="reblogs" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="reblog" inverseEntity="Status" syncable="YES"/>
@ -137,15 +144,16 @@
</entity>
<entity name="Timeline" representedClassName="TimelineMO" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="timelines" inverseEntity="Account" syncable="YES"/>
<relationship name="boundaries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimelineBoundary" inverseName="timeline" inverseEntity="TimelineBoundary" syncable="YES"/>
<relationship name="session" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Session" inverseName="selectedTimeline" inverseEntity="Session" syncable="YES"/>
<relationship name="session" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Session" inverseName="timeline" inverseEntity="Session" syncable="YES"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="timelines" inverseEntity="Status" syncable="YES"/>
</entity>
<entity name="TimelineBoundary" representedClassName="TimelineBoundaryMO" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="fetchedAt" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="start" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statusID" attributeType="String" syncable="YES"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="boundaries" inverseEntity="Status" syncable="YES"/>
<relationship name="timeline" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Timeline" inverseName="boundaries" inverseEntity="Timeline" syncable="YES"/>
</entity>
<elements>
@ -158,9 +166,10 @@
<element name="ISLanguage" positionX="-286.21875" positionY="512.6171875" width="128" height="75"/>
<element name="Mention" positionX="-441" positionY="234" width="128" height="120"/>
<element name="Session" positionX="-445.046875" positionY="277.31640625" width="128" height="150"/>
<element name="Status" positionX="-459" positionY="216" width="128" height="390"/>
<element name="Status" positionX="-459" positionY="216" width="128" height="405"/>
<element name="Tag" positionX="-432" positionY="243" width="128" height="90"/>
<element name="Timeline" positionX="-468" positionY="207" width="128" height="120"/>
<element name="Timeline" positionX="-468" positionY="207" width="128" height="135"/>
<element name="TimelineBoundary" positionX="-468" positionY="207" width="128" height="105"/>
<element name="Emoji" positionX="-468" positionY="207" width="128" height="105"/>
</elements>
</model>

6
elpha-ios/InstanceViewController.swift

@ -34,7 +34,7 @@ class InstanceViewController: UIViewController, SFSafariViewControllerDelegate {
}
@objc func presentSafariViewController() {
guard let instance = instance, let url = instance.url else {
guard let url = instance?.url else {
return
}
@ -49,11 +49,11 @@ class InstanceViewController: UIViewController, SFSafariViewControllerDelegate {
webViewButton.layer.cornerRadius = 15
if let instance = instance, let name = instance.name {
if let name = instance?.name {
navigationItem.title = name
}
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "Globe"), style: .plain, target: self, action: #selector(presentSafariViewController))
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "Globe White"), style: .plain, target: self, action: #selector(presentSafariViewController))
}
override func viewWillAppear(_ animated: Bool) {

24
elpha-ios/InstancesDataManager.swift

@ -19,7 +19,7 @@ class InstancesDataManager {
var finished = false
func upsertLanguage(string: String) -> ISLanguageMO? {
let context = CoreDataManager.shared.getContext()
let context = CoreDataManager.shared.context
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
let request = NSFetchRequest<ISLanguageMO>(entityName: "ISLanguage")
@ -42,7 +42,7 @@ class InstancesDataManager {
}
func upsertCategory(string: String) -> ISCategoryMO? {
let context = CoreDataManager.shared.getContext()
let context = CoreDataManager.shared.context
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
let request = NSFetchRequest<ISCategoryMO>(entityName: "ISCategory")
@ -123,11 +123,13 @@ class InstancesDataManager {
}
func clearInstances() {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "ISInstance")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
let request = NSFetchRequest<ISInstanceMO>(entityName: "ISInstance")
do {
try CoreDataManager.shared.getContext().execute(deleteRequest)
let instances = try CoreDataManager.shared.context.fetch(request)
instances.forEach { instance in
CoreDataManager.shared.context.delete(instance)
}
} catch {
print("\(error)")
}
@ -153,17 +155,15 @@ class InstancesDataManager {
params.append("min_id=\(nextID)")
}
let requestURL = "\(Config.instancesServiceUrl)\(Config.instancesServiceEndpoint)?\(params.joined(separator: "&"))"
let requestURL = "\(Config.instancesServiceURL)\(Config.instancesServiceListEndpoint)?\(params.joined(separator: "&"))"
let headers: HTTPHeaders = ["Authorization": "Bearer \(Config.instancesServiceSecret)"]
print("loading instances: requestURL: \(requestURL)")
Alamofire.request(requestURL, headers: headers).validate().responseJSON { response in
switch response.result {
case .success:
let context = CoreDataManager.shared.getContext()
guard let result = response.result.value as? [String: Any],
case .success(let value):
guard let result = value as? [String: Any],
let pagination = result["pagination"] as? [String: Any],
let instances = result["instances"] as? [Any] else {
completion(NSError())
@ -183,11 +183,11 @@ class InstancesDataManager {
let request = NSFetchRequest<ISInstanceMO>(entityName: "ISInstance")
request.predicate = NSPredicate(format: "id == %@", id)
let results: [ISInstanceMO] = try context.fetch(request)
let results: [ISInstanceMO] = try CoreDataManager.shared.context.fetch(request)
if let i = results.first {
self.setAttributes(on: i, with: instance)
} else {
let i = ISInstanceMO(context: context)
let i = ISInstanceMO(context: CoreDataManager.shared.context)
self.setAttributes(on: i, with: instance)
}
}

134
elpha-ios/InstancesTableViewController.swift

@ -10,42 +10,58 @@ import Alamofire
import CoreData
import UIKit
class InstancesTableViewController: UITableViewController, UIViewControllerPreviewingDelegate {
class InstancesTableViewController: UITableViewController, UIViewControllerPreviewingDelegate, NSFetchedResultsControllerDelegate {
@IBOutlet var mainNavigationItem: UINavigationItem!
var fetchedResultsController: NSFetchedResultsController<ISInstanceMO>? = nil
var loading: Bool = false {
didSet {
DispatchQueue.main.async {
if self.loading {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
self.refreshControl?.beginRefreshing()
} else {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
self.refreshControl?.endRefreshing()
}
}
}
}
var instanceCount: Int {
get {
do {
return try CoreDataManager.shared.getContext().count(for: self.getInstancesRequest())
} catch {
return 0
}
func initializeFetchedResultsController() {
let request = NSFetchRequest<ISInstanceMO>(entityName: "ISInstance")
request.sortDescriptors = [
NSSortDescriptor(key: "users", ascending: false),
]
fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: CoreDataManager.shared.context,
sectionNameKeyPath: nil,
cacheName: nil
)
fetchedResultsController!.delegate = self
do {
try fetchedResultsController!.performFetch()
} catch {
print("\(error)")
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
initializeFetchedResultsController()
mainNavigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
registerForPreviewing(with: self, sourceView: tableView)
refreshControl?.addTarget(self, action: #selector(self.refetchInstances), for: .valueChanged)
if instanceCount == 0 {
if tableView.numberOfRows(inSection: 0) == 0 {
fetchInstances()
}
}
@ -59,18 +75,10 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
DispatchQueue.main.async {
InstancesDataManager.shared.clearInstances()
self.tableView.reloadData()
}
InstancesDataManager.shared.reloadInstances() { error in
self.loading = false
guard error == nil else {
return
}
CoreDataManager.shared.saveContext()
DispatchQueue.main.async {
self.tableView.reloadData()
InstancesDataManager.shared.reloadInstances() { error in
self.loading = false
}
}
}
@ -78,42 +86,23 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
func fetchInstances() {
loading = true
InstancesDataManager.shared.loadInstances() { error in
self.loading = false
guard error == nil else {
return
}
DispatchQueue.main.async {
self.tableView.reloadData()
DispatchQueue.main.async {
InstancesDataManager.shared.loadInstances() { error in
self.loading = false
}
}
}
func getInstancesRequest() -> NSFetchRequest<ISInstanceMO> {
let request = NSFetchRequest<ISInstanceMO>(entityName: "ISInstance")
let sort = NSSortDescriptor(key: "users", ascending: false)
request.sortDescriptors = [sort]
return request
}
func getInstance(for indexPath: IndexPath) -> ISInstanceMO? {
do {
let instances = try CoreDataManager.shared.getContext().fetch(self.getInstancesRequest())
return instances[indexPath.row]
} catch {
return nil
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return instanceCount
guard let count = fetchedResultsController?.fetchedObjects?.count else {
return 0
}
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
@ -121,32 +110,35 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
fatalError("Unable to find reusable cell")
}
do {
let instances = try CoreDataManager.shared.getContext().fetch(self.getInstancesRequest())
let instance = instances[indexPath.row]
cell.instanceNameLabel.text = instance.name
cell.statusesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: instance.statuses), number: .decimal)
cell.usersLabel.text = NumberFormatter.localizedString(from: NSNumber(value: instance.users), number: .decimal)
if let thumbnail = instance.thumbnail {
cell.thumbnailImageView.af_setImage(withURL: thumbnail)
}
if indexPath.row == instances.count - 1 && !loading && !InstancesDataManager.shared.finished {
fetchInstances()
}
guard let instance = fetchedResultsController?.object(at: indexPath) else {
fatalError("CoreData error")
}
let instanceCount = tableView.numberOfRows(inSection: 0)
return cell
} catch {
return cell
cell.instanceNameLabel.text = instance.name
cell.statusesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: instance.statuses), number: .decimal)
cell.usersLabel.text = NumberFormatter.localizedString(from: NSNumber(value: instance.users), number: .decimal)
if let thumbnail = instance.thumbnail {
cell.thumbnailImageView.af_setImage(withURL: thumbnail)
}
if indexPath.row == instanceCount - 2 && !loading && !InstancesDataManager.shared.finished {
fetchInstances()
}
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "InstanceDetailSegue", let cell = segue.destination as? InstanceViewController {
if let indexPath = tableView.indexPathForSelectedRow {
cell.instance = getInstance(for: indexPath)
guard let instance = fetchedResultsController?.object(at: indexPath) else {
fatalError("CoreData error")
}
cell.instance = instance
}
}
}
@ -156,9 +148,13 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
return nil
}
guard let instance = fetchedResultsController?.object(at: indexPath) else {
fatalError("CoreData error")
}
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let detailViewController = storyboard.instantiateViewController(withIdentifier: "InstanceViewController") as? InstanceViewController {
detailViewController.instance = getInstance(for: indexPath)
detailViewController.instance = instance
return detailViewController
} else {
return nil

2
elpha-ios/MainTabBarController.swift

@ -12,7 +12,7 @@ class MainTabBarController: UITabBarController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if AuthenticationManager.shared.sessionCount == 0 {
if AuthenticationManager.shared.sessions.count == 0 {
performSegue(withIdentifier: "AuthenticateSegue", sender: self)
return
}

292
elpha-ios/MastodonDataManager.swift

@ -21,37 +21,36 @@ class UpsertResult<T> {
}
public class MastodonDataManager {
static func upsertAccount(_ remoteAccount: Account) -> AccountMO? {
func saveAccount(_ account: AccountMO) -> AccountMO? {
account.id = remoteAccount.id
account.username = remoteAccount.username
account.acct = remoteAccount.acct
account.displayName = remoteAccount.displayName
account.note = remoteAccount.note
account.url = remoteAccount.url
account.avatarURL = URL(string: remoteAccount.avatar)
account.avatarStaticURL = URL(string: remoteAccount.avatarStatic)
account.headerURL = URL(string: remoteAccount.header)
account.headerStaticURL = URL(string: remoteAccount.headerStatic)
account.locked = remoteAccount.locked
account.createdAt = remoteAccount.createdAt
account.followersCount = Int32(remoteAccount.followersCount)
account.followingCount = Int32(remoteAccount.followingCount)
account.statusesCount = Int32(remoteAccount.statusesCount)
return account
}
static func setAccount(_ account: AccountMO, withRemoteAccount remoteAccount: Account) -> AccountMO {
account.id = remoteAccount.id
account.username = remoteAccount.username
account.acct = remoteAccount.acct
account.displayName = remoteAccount.displayName
account.note = remoteAccount.note
account.url = remoteAccount.url
account.avatarURL = URL(string: remoteAccount.avatar)
account.avatarStaticURL = URL(string: remoteAccount.avatarStatic)
account.headerURL = URL(string: remoteAccount.header)
account.headerStaticURL = URL(string: remoteAccount.headerStatic)
account.locked = remoteAccount.locked
account.createdAt = remoteAccount.createdAt
account.followersCount = Int32(remoteAccount.followersCount)
account.followingCount = Int32(remoteAccount.followingCount)
account.statusesCount = Int32(remoteAccount.statusesCount)
let context = CoreDataManager.shared.getContext()
return account
}
static func upsertAccount(_ remoteAccount: Account) -> AccountMO? {
let request = NSFetchRequest<AccountMO>(entityName: "Account")
request.predicate = NSPredicate(format: "acct == %@", remoteAccount.acct)
request.predicate = NSPredicate(format: "id == %@", remoteAccount.id)
do {
let results = try context.fetch(request)
let results = try CoreDataManager.shared.context.fetch(request)
if let account = results.first {
return saveAccount(account)
return setAccount(account, withRemoteAccount: remoteAccount)
} else {
return saveAccount(AccountMO(context: context))
return setAccount(AccountMO(context: CoreDataManager.shared.context), withRemoteAccount: remoteAccount)
}
} catch {
print("\(error)")
@ -59,28 +58,27 @@ public class MastodonDataManager {
}
}
static func upsertAttachment(_ remoteAttachment: Attachment) -> AttachmentMO? {
func saveAttachment(_ attachment: AttachmentMO) -> AttachmentMO? {
attachment.id = remoteAttachment.id
attachment.type = remoteAttachment.type.rawValue
attachment.url = URL(string: remoteAttachment.url)
attachment.previewURL = URL(string: remoteAttachment.previewURL)
attachment.remoteURL = remoteAttachment.remoteURL
attachment.textURL = remoteAttachment.textURL
return attachment
}
static func setAttachment(_ attachment: AttachmentMO, withRemoteAttachment remoteAttachment: Attachment) -> AttachmentMO {
attachment.id = remoteAttachment.id
attachment.type = remoteAttachment.type.rawValue
attachment.url = URL(string: remoteAttachment.url)
attachment.previewURL = URL(string: remoteAttachment.previewURL)
attachment.remoteURL = remoteAttachment.remoteURL
attachment.textURL = remoteAttachment.textURL
let context = CoreDataManager.shared.getContext()
return attachment
}
static func upsertAttachment(_ remoteAttachment: Attachment) -> AttachmentMO? {
let request = NSFetchRequest<AttachmentMO>(entityName: "Attachment")
request.predicate = NSPredicate(format: "id == %@", remoteAttachment.id)
do {
let results = try context.fetch(request)
let results = try CoreDataManager.shared.context.fetch(request)
if let attachment = results.first {
return saveAttachment(attachment)
return setAttachment(attachment, withRemoteAttachment: remoteAttachment)
} else {
return saveAttachment(AttachmentMO(context: context))
return setAttachment(AttachmentMO(context: CoreDataManager.shared.context), withRemoteAttachment: remoteAttachment)
}
} catch {
print("\(error)")
@ -88,26 +86,25 @@ public class MastodonDataManager {
}
}
static func upsertMention(_ remoteMention: Mention) -> MentionMO? {
func saveMention(_ mention: MentionMO) -> MentionMO? {
mention.id = remoteMention.id
mention.username = remoteMention.username
mention.acct = remoteMention.acct
mention.url = URL(string: remoteMention.url)
return mention
}
static func setMention(_ mention: MentionMO, withRemoteMention remoteMention: Mention) -> MentionMO {
mention.id = remoteMention.id
mention.username = remoteMention.username
mention.acct = remoteMention.acct
mention.url = URL(string: remoteMention.url)
let context = CoreDataManager.shared.getContext()
return mention
}
static func upsertMention(_ remoteMention: Mention) -> MentionMO? {
let request = NSFetchRequest<MentionMO>(entityName: "Mention")
request.predicate = NSPredicate(format: "id == %@", remoteMention.id)
do {
let results = try context.fetch(request)
let results = try CoreDataManager.shared.context.fetch(request)
if let mention = results.first {
return saveMention(mention)
return setMention(mention, withRemoteMention: remoteMention)
} else {
return saveMention(MentionMO(context: context))
return setMention(MentionMO(context: CoreDataManager.shared.context), withRemoteMention: remoteMention)
}
} catch {
print("\(error)")
@ -115,24 +112,23 @@ public class MastodonDataManager {
}
}
static func upsertTag(_ remoteTag: Tag) -> TagMO? {
func saveTag(_ tag: TagMO) -> TagMO? {
tag.name = remoteTag.name
tag.url = URL(string: remoteTag.url)
return tag
}
static func setTag(_ tag: TagMO, withRemoteTag remoteTag: Tag) -> TagMO {
tag.name = remoteTag.name
tag.url = URL(string: remoteTag.url)
let context = CoreDataManager.shared.getContext()
return tag
}
static func upsertTag(_ remoteTag: Tag) -> TagMO? {
let request = NSFetchRequest<TagMO>(entityName: "Tag")
request.predicate = NSPredicate(format: "name == %@", remoteTag.name)
do {
let results = try context.fetch(request)
let results = try CoreDataManager.shared.context.fetch(request)
if let tag = results.first {
return saveTag(tag)
return setTag(tag, withRemoteTag: remoteTag)
} else {
return saveTag(TagMO(context: context))
return setTag(TagMO(context: CoreDataManager.shared.context), withRemoteTag: remoteTag)
}
} catch {
print("\(error)")
@ -140,27 +136,26 @@ public class MastodonDataManager {
}
}
static func upsertApp(_ remoteApp: Application) -> AppMO? {
func saveApp(_ app: AppMO) -> AppMO? {
app.name = remoteApp.name
if let website = remoteApp.website {
app.website = URL(string: website)
}
return app
static func setApp(_ app: AppMO, withRemoteApp remoteApp: Application) -> AppMO {
app.name = remoteApp.name
if let website = remoteApp.website {
app.website = URL(string: website)
}
let context = CoreDataManager.shared.getContext()
return app
}
static func upsertApp(_ remoteApp: Application) -> AppMO? {
let request = NSFetchRequest<AppMO>(entityName: "App")
request.predicate = NSPredicate(format: "name == %@", remoteApp.name)
do {
let results = try context.fetch(request)
if let tag = results.first {
return saveApp(tag)
let results = try CoreDataManager.shared.context.fetch(request)
if let app = results.first {
return setApp(app, withRemoteApp: remoteApp)
} else {
return saveApp(AppMO(context: context))
return setApp(AppMO(context: CoreDataManager.shared.context), withRemoteApp: remoteApp)
}
} catch {
print("\(error)")
@ -168,65 +163,102 @@ public class MastodonDataManager {
}
}
static func upsertStatus(_ remoteStatus: Status) -> UpsertResult<StatusMO>? {
func saveStatus(_ status: StatusMO) -> StatusMO {
status.id = remoteStatus.id
status.uri = URL(string: remoteStatus.uri)
status.url = remoteStatus.url
status.account = MastodonDataManager.upsertAccount(remoteStatus.account)
status.inReplyToID = remoteStatus.inReplyToID
status.inReplyToAccountID = remoteStatus.inReplyToAccountID
status.content = remoteStatus.content
status.createdAt = remoteStatus.createdAt
status.reblogsCount = Int32(remoteStatus.reblogsCount)
status.favouritesCount = Int32(remoteStatus.favouritesCount)
status.reblogged = remoteStatus.reblogged ?? false
status.favourited = remoteStatus.favourited ?? false
status.sensitive = remoteStatus.sensitive ?? false
status.pinned = remoteStatus.pinned ?? false
status.spoilerText = remoteStatus.spoilerText
status.visibility = remoteStatus.visibility.rawValue
if let app = remoteStatus.application {
status.app = MastodonDataManager.upsertApp(app)
static func setEmoji(_ emoji: EmojiMO, withRemoteEmoji remoteEmoji: Emoji) -> EmojiMO {
emoji.shortcode = remoteEmoji.shortcode
emoji.staticURL = remoteEmoji.staticURL
emoji.url = remoteEmoji.url
emoji.visibleInPicker = false
return emoji
}
static func upsertEmoji(_ remoteEmoji: Emoji) -> EmojiMO? {
let request = NSFetchRequest<EmojiMO>(entityName: "Emoji")
request.predicate = NSPredicate(format: "url == %@", remoteEmoji.url.absoluteString)
do {
let results = try CoreDataManager.shared.context.fetch(request)
if let emoji = results.first {
return setEmoji(emoji, withRemoteEmoji: remoteEmoji)
} else {
return setEmoji(EmojiMO(context: CoreDataManager.shared.context), withRemoteEmoji: remoteEmoji)
}
remoteStatus.mediaAttachments.forEach { attachment in
if let attachment = MastodonDataManager.upsertAttachment(attachment) {
status.mutableOrderedSetValue(forKey: "attachments").add(attachment)
}
} catch {
print("\(error)")
return nil
}
}
static func setStatus(_ status: StatusMO, withRemoteStatus remoteStatus: Status) -> StatusMO {
status.id = remoteStatus.id
status.uri = URL(string: remoteStatus.uri)
status.url = remoteStatus.url
status.account = MastodonDataManager.upsertAccount(remoteStatus.account)
status.inReplyToID = remoteStatus.inReplyToID
status.inReplyToAccountID = remoteStatus.inReplyToAccountID
status.content = remoteStatus.content
status.createdAt = remoteStatus.createdAt
status.reblogsCount = Int32(remoteStatus.reblogsCount)
status.favouritesCount = Int32(remoteStatus.favouritesCount)
status.reblogged = remoteStatus.reblogged ?? false
status.favourited = remoteStatus.favourited ?? false
status.sensitive = remoteStatus.sensitive ?? false
status.pinned = remoteStatus.pinned ?? false
status.spoilerText = remoteStatus.spoilerText
status.visibility = remoteStatus.visibility.rawValue
if let app = remoteStatus.application {
status.app = MastodonDataManager.upsertApp(app)
}
remoteStatus.mediaAttachments.forEach { attachment in
if let attachment = MastodonDataManager.upsertAttachment(attachment) {
status.mutableOrderedSetValue(forKey: "attachments").add(attachment)
}
remoteStatus.mentions.forEach { mention in
if let mention = MastodonDataManager.upsertMention(mention) {
status.mutableSetValue(forKey: "mentions").add(mention)
}
}
remoteStatus.mentions.forEach { mention in
if let mention = MastodonDataManager.upsertMention(mention) {
status.mutableSetValue(forKey: "mentions").add(mention)
}
remoteStatus.tags.forEach { tag in
if let tag = MastodonDataManager.upsertTag(tag) {
status.mutableSetValue(forKey: "tags").add(tag)
}
}
remoteStatus.tags.forEach { tag in
if let tag = MastodonDataManager.upsertTag(tag) {
status.mutableSetValue(forKey: "tags").add(tag)
}
if let reblog = remoteStatus.reblog {
let savedReblogResult = upsertStatus(reblog)
status.reblog = savedReblogResult?.model
}
remoteStatus.emojis.forEach { emoji in
if let emoji = MastodonDataManager.upsertEmoji(emoji) {
status.mutableSetValue(forKey: "emojis").add(emoji)
}
return status
}
let context = CoreDataManager.shared.getContext()
if let reblog = remoteStatus.reblog {
let savedReblogResult = upsertStatus(reblog)
status.reblog = savedReblogResult?.model
}
return status
}
static func upsertStatus(_ remoteStatus: Status) -> UpsertResult<StatusMO>? {
let request = NSFetchRequest<StatusMO>(entityName: "Status")
request.predicate = NSPredicate(format: "id == %@", remoteStatus.id)
do {
let results = try context.fetch(request)
let results = try CoreDataManager.shared.context.fetch(request)
if let status = results.first {
return UpsertResult(model: saveStatus(status), new: false)
return UpsertResult(
model: setStatus(status, withRemoteStatus: remoteStatus),
new: false
)
} else {
return UpsertResult(model: saveStatus(StatusMO(context: context)), new: true)
return UpsertResult(
model: setStatus(StatusMO(context: CoreDataManager.shared.context), withRemoteStatus: remoteStatus),
new: true
)
}
} catch {
print("\(error)")
@ -235,7 +267,7 @@ public class MastodonDataManager {
}
static func saveClient(id: String, clientID: String, clientSecret: String, url: String) -> ClientMO {
let client = ClientMO(context: CoreDataManager.shared.getContext())
let client = ClientMO(context: CoreDataManager.shared.context)
client.id = id
client.clientID = clientID
client.clientSecret = clientSecret
@ -245,12 +277,12 @@ public class MastodonDataManager {
return client
}
static func getAccountByID(_ id: String) -> AccountMO? {
static func accountByID(_ id: String) -> AccountMO? {
let request = NSFetchRequest<AccountMO>(entityName: "Account")
request.predicate = NSPredicate(format: "id == %@", id)
do {
let results = try CoreDataManager.shared.getContext().fetch(request)
let results = try CoreDataManager.shared.context.fetch(request)
guard let account = results.first else {
return nil
}
@ -262,13 +294,13 @@ public class MastodonDataManager {
}
}
static func getRemoteAccountByID(id: String, completion: @escaping (AccountMO?, Error?) -> Void ) {
if let account = getAccountByID(id) {
static func remoteAccountByID(id: String, completion: @escaping (AccountMO?, Error?) -> Void ) {
if let account = accountByID(id) {
completion(account, nil)
return
}
guard let client = AuthenticationManager.shared.mkClientForSelectedSession() else {
guard let client = AuthenticationManager.shared.mkClient else {
completion(nil, NSError())
return
}

94
elpha-ios/StatusView.swift

@ -6,6 +6,7 @@
// Copyright © 2018 Elpha. All rights reserved.
//
import AlamofireImage
import UIKit
class StatusView: UIView {
@ -52,4 +53,97 @@ class StatusView: UIView {
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
public func update(withStatus status: StatusMO) {
topDividerView.isHidden = false
topLoadMoreView.isHidden = true
boostView.isHidden = true
replyView.isHidden = true
bottomLoadMoreView.isHidden = true
bottomDividerView.isHidden = false
attachmentsView.backgroundColor = UIColor.white
attachmentsHeightConstraint.constant = frame.width
attachmentsView.isHidden = true
let avatarFilter = AspectScaledToFillSizeWithRoundedCornersFilter(
size: CGSize(width: 40.0, height: 40.0),
radius: 20.0,
divideRadiusByImageScale: true
)
func updateStatusContent(_ status: StatusMO) {
if let account = status.account {
avatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter)
displayNameLabel.text = account.displayName
usernameLabel.text = account.acct
}
if let attachments = status.attachments, attachments.count > 0 {
attachmentsView.isHidden = false
AttachmentsManager.setupAttachmentsView(attachmentsView, withAttachments: attachments)
}
if let content = status.content {
do {
let styledContent = "<style>html * { font-size: 15px; color: #170c49; font-family: -apple-system } p { margin: 0; padding: 0 }</style> \(content)"
let attributedText = try NSAttributedString(
data: styledContent.data(using: String.Encoding.unicode, allowLossyConversion: true)!,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
)
contentLabel.attributedText = attributedText
} catch {
print("\(error)")
}
}
timestampLabel.text = status.createdAt!.timeAgo()
repliesLabel.text = "0"
boostsLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.reblogsCount), number: .decimal)
favoritesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.favouritesCount), number: .decimal)
if status.reblogged {
boostsImageView.image = UIImage(named: "Boost Bold")
} else {
boostsImageView.image = UIImage(named: "Boost Regular")
}
if status.favourited {
favoritesImageView.image = UIImage(named: "Star Filled")
} else {
favoritesImageView.image = UIImage(named: "Star Regular")
}
}
if let reblog = status.reblog {
boostView.isHidden = false
if let account = status.account {
boostAvatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter)
boostDisplayNameLabel.text = account.displayName
boostUsernameLabel.text = account.acct
}
updateStatusContent(reblog)
} else {
if let account = status.account {
avatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter)
displayNameLabel.text = account.displayName
usernameLabel.text = account.acct
}
updateStatusContent(status)
}
if let replyAccountID = status.inReplyToAccountID {
if let replyAccount = MastodonDataManager.accountByID(replyAccountID) {
replyView.isHidden = false
replyAvatarImageView.af_setImage(withURL: replyAccount.avatarURL!, filter: avatarFilter)
replyDisplayNameLabel.text = replyAccount.displayName
replyUsernameLabel.text = replyAccount.acct
}
}
}
}

334
elpha-ios/TimelineTableViewController.swift

@ -11,18 +11,16 @@ import CoreData
import MastodonKit
import UIKit
class TimelineTableViewController: UITableViewController {
class TimelineTableViewController: UITableViewController, TimelinesTableViewControllerDelegate {
var fetchedResultsController: NSFetchedResultsController<StatusMO>? = nil
let fetchLimit = 50
let cellHeightsDictionary = NSMutableDictionary()
var loading: Bool = false {
didSet {
DispatchQueue.main.async {
if self.loading {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
self.refreshControl?.beginRefreshing()
} else {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
self.refreshControl?.endRefreshing()
}
}
@ -32,15 +30,22 @@ class TimelineTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
initializeFetchedResultsController()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 500
let cleanButtonItem = UIBarButtonItem(image: UIImage(named: "Refresh CCW"), style: .plain, target: self, action: #selector(clean))
let moreButtonItem = UIBarButtonItem(image: UIImage(named: "More"), style: .plain, target: self, action: #selector(more))
let cleanButtonItem = UIBarButtonItem(image: UIImage(named: "Refresh CCW"), style: .plain, target: self, action: #selector(clean))
let composeButtonItem = UIBarButtonItem(image: UIImage(named: "Compose"), style: .plain, target: self, action: #selector(compose))
navigationItem.rightBarButtonItems = [composeButtonItem, moreButtonItem, cleanButtonItem]
navigationItem.title = "Home"
navigationItem.rightBarButtonItems = [composeButtonItem, cleanButtonItem, moreButtonItem]
if let timeline = AuthenticationManager.shared.session?.timeline {
navigationItem.title = timeline.name
} else {
navigationItem.title = "Home"
}
refreshControl?.addTarget(self, action: #selector(self.fetchTimelineWithDefaultRange), for: .valueChanged)
}
@ -51,19 +56,23 @@ class TimelineTableViewController: UITableViewController {
}
@objc func clean() {
guard let session = AuthenticationManager.shared.selectedSession, let timeline = session.selectedTimeline else {
guard let timeline = AuthenticationManager.shared.session?.timeline else {
return
}
timeline.mutableSetValue(forKey: "statuses").removeAllObjects()
if let boundaries = timeline.boundaries?.allObjects as? [TimelineBoundaryMO] {
boundaries.forEach { CoreDataManager.shared.context.delete($0) }
}
timeline.mutableSetValue(forKey: "boundaries").removeAllObjects()
CoreDataManager.shared.saveContext()
self.tableView.reloadData()
}
@objc func more() {
performSegue(withIdentifier: "TimelinesSegue", sender: self)
}
@objc func compose() {
@ -72,25 +81,43 @@ class TimelineTableViewController: UITableViewController {
present(alertController, animated: true)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "TimelinesSegue" {
if let destination = segue.destination as? TimelinesTableViewController {
destination.delegate = self
}
}
}
func didSelect(timeline: TimelineMO) {
navigationItem.title = timeline.name
AuthenticationManager.shared.session?.timeline = timeline
CoreDataManager.shared.saveContext()
initializeFetchedResultsController()
tableView.reloadData()
}
func createDefaultTimelines(account: AccountMO) {
let context = CoreDataManager.shared.getContext()
let timelineNames = [
let names = [
"Home",
"Local",
"Federated",
"Favorites",
]
timelineNames.forEach { timelineName in
let timeline = TimelineMO(context: context)
timeline.name = timelineName
for (index, name) in names.enumerated() {
let timeline = TimelineMO(context: CoreDataManager.shared.context)
timeline.name = name
timeline.account = account
timeline.order = Int16(index)
}
CoreDataManager.shared.saveContext()
}
func fetchStatuses(request: Request<[Status]>, forTimeline timeline: TimelineMO, completion: @escaping ([UpsertResult<StatusMO>], Error?) -> Void) {
guard let client = AuthenticationManager.shared.mkClientForSelectedSession() else {
guard let client = AuthenticationManager.shared.mkClient else {
completion([], nil)
return
}
@ -101,33 +128,38 @@ class TimelineTableViewController: UITableViewController {
switch result {
case .success(let remoteStatuses, _):
DispatchQueue.main.async {
let context = CoreDataManager.shared.getContext()
let statuses = remoteStatuses.compactMap { status in
return MastodonDataManager.upsertStatus(status)
}
for (index, statusResult) in statuses.enumerated() {
if index == 0 {
let boundary = TimelineBoundaryMO(context: context)
boundary.statusID = statusResult.model.id!
let boundary = TimelineBoundaryMO(context: CoreDataManager.shared.context)
boundary.status = statusResult.model
boundary.timeline = timeline
boundary.start = true
boundary.createdAt = Date()
timeline.mutableSetValue(forKey: "boundaries").add(boundary)
} else if index == statuses.count {
let boundary = TimelineBoundaryMO(context: context)
boundary.statusID = statusResult.model.id!
boundary.fetchedAt = Date()
} else if index == statuses.count - 1 {
let boundary = TimelineBoundaryMO(context: CoreDataManager.shared.context)
boundary.status = statusResult.model
boundary.timeline = timeline
boundary.start = false
boundary.createdAt = Date()
timeline.mutableSetValue(forKey: "boundaries").add(boundary)
boundary.fetchedAt = Date()
} else {
let predicate = NSPredicate(format: "statusID != %@", statusResult.model.id!)
timeline.mutableSetValue(forKey: "boundaries").filter(using: predicate)
let request = NSFetchRequest<TimelineBoundaryMO>(entityName: "TimelineBoundary")
request.predicate = NSPredicate(format: "status == %@ AND timeline == %@", statusResult.model, timeline)
do {
let results = try CoreDataManager.shared.context.fetch(request)
results.forEach { CoreDataManager.shared.context.delete($0) }
} catch {
print("\(error)")
}
}
statusResult.model.addToTimelines(timeline)
}
// timeline.addToStatuses(NSSet(array: statuses))
CoreDataManager.shared.saveContext()
self.loading = false
@ -148,12 +180,12 @@ class TimelineTableViewController: UITableViewController {
}
func fetchTimeline(withRange requestRange: RequestRange, completion: @escaping (Error?) -> Void) {
guard let session = AuthenticationManager.shared.selectedSession, let account = session.account else {
guard let session = AuthenticationManager.shared.session, let account = session.account else {
completion(nil)
return
}
if session.selectedTimeline == nil {
if session.timeline == nil {
if let timelines = account.timelines, timelines.count == 0 {
createDefaultTimelines(account: account)
}
@ -162,15 +194,15 @@ class TimelineTableViewController: UITableViewController {
request.predicate = NSPredicate(format: "name == %@", "Home")
do {
let results = try CoreDataManager.shared.getContext().fetch(request)
session.selectedTimeline = results.first
let results = try CoreDataManager.shared.context.fetch(request)
session.timeline = results.first
CoreDataManager.shared.saveContext()
} catch {
print("\(error)")
}
}
guard let selectedTimeline = session.selectedTimeline else {
guard let timeline = session.timeline else {
completion(nil)
return
}
@ -179,18 +211,20 @@ class TimelineTableViewController: UITableViewController {
var request: Request<[Status]>
switch selectedTimeline.name {
switch timeline.name {
case "Home":
request = Timelines.home(range: requestRange)
case "Local":
request = Timelines.public(local: true, range: requestRange)
case "Federated":
request = Timelines.public(local: false, range: requestRange)
case "Favorites":
request = Favourites.all(range: requestRange)
case let tag:
request = Timelines.tag(tag!, range: requestRange)
}
fetchStatuses(request: request, forTimeline: selectedTimeline) { statuses, error in
fetchStatuses(request: request, forTimeline: timeline) { statuses, error in
guard error == nil else {
completion(error)
return
@ -218,75 +252,50 @@ class TimelineTableViewController: UITableViewController {
}
}
extension TimelineTableViewController {
func timelineRequest() -> NSFetchRequest<TimelineMO>? {
guard let session = AuthenticationManager.shared.selectedSession, let selectedTimeline = session.selectedTimeline else {
return nil
extension TimelineTableViewController: NSFetchedResultsControllerDelegate {
func initializeFetchedResultsController() {
guard let timeline = AuthenticationManager.shared.session?.timeline else {
return
}
let request = NSFetchRequest<TimelineMO>(entityName: "Timeline")
request.predicate = NSPredicate(format: "name == %@", selectedTimeline.name!)
let request = NSFetchRequest<StatusMO>(entityName: "Status")
request.predicate = NSPredicate(format: "ANY timelines = %@", timeline)
request.sortDescriptors = [
NSSortDescriptor(key: "createdAt", ascending: false),
]
return request
}
func timelineStatuses() -> [StatusMO]? {
guard let request = timelineRequest() else {
return []
}
fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: CoreDataManager.shared.context,
sectionNameKeyPath: nil,
cacheName: nil
)
fetchedResultsController!.delegate = self
do {
let response = try CoreDataManager.shared.getContext().fetch(request)
guard let timeline = response.first, let statuses = timeline.statuses else {
return []
}
let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
return statuses.sortedArray(using: [sortDescriptor]) as? [StatusMO]
try fetchedResultsController!.performFetch()
} catch {
print("\(error)")
return []
}
}
func timelineStatusesCount() -> Int {
guard let statuses = timelineStatuses() else {
return 0
}
return statuses.count
}
func timelineBoundaries() -> NSSet {
guard let session = AuthenticationManager.shared.selectedSession,
let timeline = session.selectedTimeline,
let boundaries = timeline.boundaries else {
return NSSet()
}
return boundaries
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.reloadData()
}
}
extension TimelineTableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timelineStatusesCount()
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightsDictionary.setValue(cell.frame.size.height, forKey: String(indexPath.row))
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cellHeightsDictionary.object(forKey: String(indexPath.row)) as? CGFloat {
return height
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let count = fetchedResultsController?.fetchedObjects?.count else {
return 0
}
return UITableView.automaticDimension
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
@ -294,36 +303,26 @@ extension TimelineTableViewController {
fatalError("Unable to find reusable cell")
}
cell.statusView.topDividerView.isHidden = false
cell.statusView.topLoadMoreView.isHidden = true
cell.statusView.boostView.isHidden = true
cell.statusView.replyView.isHidden = true
cell.statusView.bottomLoadMoreView.isHidden = true
cell.statusView.bottomDividerView.isHidden = false
guard let status = fetchedResultsController?.object(at: indexPath), let statuses = fetchedResultsController?.fetchedObjects else {
fatalError("CoreData error")
}
cell.statusView.attachmentsView.backgroundColor = UIColor.white
cell.statusView.attachmentsHeightConstraint.constant = cell.frame.width
cell.statusView.attachmentsView.isHidden = true
guard let timeline = AuthenticationManager.shared.session?.timeline else {
fatalError("No timeline")
}
if let statuses = timelineStatuses() {
let status = statuses[indexPath.row]
let boundaries = timelineBoundaries()
let avatarFilter = AspectScaledToFillSizeWithRoundedCornersFilter(
size: CGSize(width: 40.0, height: 40.0),
radius: 20.0,
divideRadiusByImageScale: true
)
cell.statusView.update(withStatus: status)
let timelinePredicate = NSPredicate(format: "timeline = %@", timeline)
if let boundary = status.boundaries?.filtered(using: timelinePredicate).first as? TimelineBoundaryMO {
if indexPath.row != 0 {
if let boundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", status.id!)).first as? TimelineBoundaryMO {
if boundary.start {
let previousStatus = statuses[indexPath.row - 1]
if let previousBoundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", previousStatus.id!)).first as? TimelineBoundaryMO {
if !previousBoundary.start {
cell.statusView.topDividerView.isHidden = true
cell.statusView.topLoadMoreView.isHidden = false
}
if boundary.start {
let previousStatus = statuses[indexPath.row - 1]
if let previousBoundary = previousStatus.boundaries?.filtered(using: timelinePredicate).first as? TimelineBoundaryMO {
if !previousBoundary.start {
cell.statusView.topDividerView.isHidden = true
cell.statusView.topLoadMoreView.isHidden = false
}
}
}
@ -332,107 +331,28 @@ extension TimelineTableViewController {
}
if indexPath.row < statuses.count - 1 {
if let boundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", status.id!)).first as? TimelineBoundaryMO {
if !boundary.start {
let nextStatus = statuses[indexPath.row + 1]
if let nextBoundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", nextStatus.id!)).first as? TimelineBoundaryMO {
if nextBoundary.start {
cell.statusView.bottomDividerView.isHidden = true
cell.statusView.bottomLoadMoreView.isHidden = false
}
if !boundary.start {
let nextStatus = statuses[indexPath.row + 1]
if let nextBoundary = nextStatus.boundaries?.filtered(using: timelinePredicate).first as? TimelineBoundaryMO {
if nextBoundary.start {
cell.statusView.bottomDividerView.isHidden = true
cell.statusView.bottomLoadMoreView.isHidden = false
}
}
}
}
func setStatusContent(_ status: StatusMO) {
if let account = status.account {
cell.statusView.avatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter)
cell.statusView.displayNameLabel.text = account.displayName
cell.statusView.usernameLabel.text = account.acct
}
if let attachments = status.attachments, attachments.count > 0 {
cell.statusView.attachmentsView.isHidden = false
AttachmentsManager.setupAttachmentsView(cell.statusView.attachmentsView, withAttachments: attachments)
}
if let content = status.content {
do {
let styledContent = "<style>html * { font-size: 15px; color: #170c49; font-family: -apple-system } p { margin: 0; padding: 0 }</style> \(content)"
let attributedText = try NSAttributedString(
data: styledContent.data(using: String.Encoding.unicode, allowLossyConversion: true)!,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
)
cell.statusView.contentLabel.attributedText = attributedText
} catch {
print("\(error)")
}
}
cell.statusView.timestampLabel.text = status.createdAt!.timeAgo()
cell.statusView.repliesLabel.text = "0"
cell.statusView.boostsLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.reblogsCount), number: .decimal)
cell.statusView.favoritesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.favouritesCount), number: .decimal)
if status.reblogged {
cell.statusView.boostsImageView.image = UIImage(named: "Boost Bold")
} else {
cell.statusView.boostsImageView.image = UIImage(named: "Boost Regular")
}
if status.favourited {
cell.statusView.favoritesImageView.image = UIImage(named: "Star Filled")
} else {
cell.statusView.favoritesImageView.image = UIImage(named: "Star Regular")
}
}
if let reblog = status.reblog {
cell.statusView.boostView.isHidden = false
if let account = status.account {
cell.statusView.boostAvatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter)
cell.statusView.boostDisplayNameLabel.text = account.displayName
cell.statusView.boostUsernameLabel.text = account.acct
}
setStatusContent(reblog)
} else {
if let account = status.account {
cell.statusView.avatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter)
cell.statusView.displayNameLabel.text = account.displayName
cell.statusView.usernameLabel.text = account.acct
}
setStatusContent(status)
}
if let replyAccountID = status.inReplyToAccountID {
if let replyAccount = MastodonDataManager.getAccountByID(replyAccountID) {
cell.statusView.replyView.isHidden = false
cell.statusView.replyAvatarImageView.af_setImage(withURL: replyAccount.avatarURL!, filter: avatarFilter)
cell.statusView.replyDisplayNameLabel.text = replyAccount.displayName
cell.statusView.replyUsernameLabel.text = replyAccount.acct
}
if indexPath.row == statuses.count - 1 {
cell.statusView.bottomDividerView.isHidden = true
}
if indexPath.row == statuses.count - 1 && !loading {
fetchTimeline(withRange: .max(id: status.id!, limit: fetchLimit)) { error in
guard error == nil else {
return
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
if indexPath.row == statuses.count - 2 && !loading {
fetchTimeline(withRange: .max(id: status.id!, limit: fetchLimit)) { error in
guard error == nil else {
return
}
}
return cell
}
return cell

4
elpha-ios/TimelinesNavigationController.swift

@ -33,11 +33,11 @@ class TimelinesNavigationController: UINavigationController {
view.addSubview(blurEffectView)
bottomLayoutConstraint = blurEffectView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 50)
bottomLayoutConstraint = blurEffectView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 100)
NSLayoutConstraint.activate([
blurEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
blurEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
blurEffectView.widthAnchor.constraint(equalToConstant: 200),
blurEffectView.heightAnchor.constraint(equalToConstant: 50),
bottomLayoutConstraint!,
])

14
elpha-ios/TimelinesTableViewCell.swift

@ -0,0 +1,14 @@
//
// TimelinesTableViewCell.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/12/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import UIKit
class TimelinesTableViewCell: UITableViewCell {
@IBOutlet var timelineImageView: UIImageView!
@IBOutlet var timelineLabel: UILabel!
}

127
elpha-ios/TimelinesTableViewController.swift

@ -0,0 +1,127 @@
//
// TimelinesTableViewController.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/12/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import CoreData
import UIKit
protocol TimelinesTableViewControllerDelegate {
func didSelect(timeline: TimelineMO)
}
class TimelinesTableViewController: UITableViewController {
var delegate: TimelinesTableViewControllerDelegate? = nil
var fetchedResultsController: NSFetchedResultsController<TimelineMO>? = nil
override func viewDidLoad() {
super.viewDidLoad()
initializeFetchedResultsController()
}
func initializeFetchedResultsController() {
let request = NSFetchRequest<TimelineMO>(entityName: "Timeline")
request.sortDescriptors = [
NSSortDescriptor(key: "order", ascending: true),
]
fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: CoreDataManager.shared.context,
sectionNameKeyPath: nil,
cacheName: nil
)
fetchedResultsController!.delegate = self
do {
try fetchedResultsController!.performFetch()
} catch {
print("\(error)")
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let count = fetchedResultsController?.fetchedObjects?.count else {
return 0
}
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TimelinesTableViewCell", for: indexPath) as? TimelinesTableViewCell else {
fatalError("Unable to find reusable cell")
}
guard let timeline = fetchedResultsController?.object(at: indexPath) else {
fatalError("CoreData error")
}
let selectedTimeline = AuthenticationManager.shared.session?.timeline
cell.timelineLabel.text = timeline.name
if selectedTimeline == timeline {
cell.accessoryType = UITableViewCell.AccessoryType.checkmark
}
switch timeline.name {
case "Home":
cell.timelineImageView.image = UIImage(named: "Home")
case "Local":
cell.timelineImageView.image = UIImage(named: "Users")
case "Federated":
cell.timelineImageView.image = UIImage(named: "Globe")
case "Favorites":
cell.timelineImageView.image = UIImage(named: "Star Regular")
default:
cell.timelineImageView.image = UIImage(named: "Tag")
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let timeline = fetchedResultsController?.object(at: indexPath) else {
fatalError("CoreData error")
}
if let delegate = self.delegate {
delegate.didSelect(timeline: timeline)
}
dismiss(animated: true)
}
}
extension TimelinesTableViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case NSFetchedResultsChangeType.insert:
tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.fade)
case NSFetchedResultsChangeType.delete:
tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.fade)
case NSFetchedResultsChangeType.update:
return
case NSFetchedResultsChangeType.move:
tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.fade)
tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.fade)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
Loading…
Cancel
Save