forked from zelo72/mastodon-ios
feat: HomeTimeline
This commit is contained in:
parent
71a485fc4a
commit
5f1800b353
|
@ -32,7 +32,7 @@
|
||||||
<attribute name="hasMore" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="hasMore" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="identifier" attributeType="String"/>
|
<attribute name="identifier" attributeType="String"/>
|
||||||
<attribute name="userID" attributeType="String"/>
|
<attribute name="userID" attributeType="String"/>
|
||||||
<relationship name="toots" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="homeTimelineIndexes" inverseEntity="Toot"/>
|
<relationship name="toot" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="homeTimelineIndexes" inverseEntity="Toot"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="MastodonAuthentication" representedClassName=".MastodonAuthentication" syncable="YES">
|
<entity name="MastodonAuthentication" representedClassName=".MastodonAuthentication" syncable="YES">
|
||||||
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
<attribute name="identifier" attributeType="String"/>
|
<attribute name="identifier" attributeType="String"/>
|
||||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="bookmarked" inverseEntity="Toot"/>
|
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="bookmarkedBy" inverseEntity="Toot"/>
|
||||||
<relationship name="favourite" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="favouritedBy" inverseEntity="Toot"/>
|
<relationship name="favourite" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="favouritedBy" inverseEntity="Toot"/>
|
||||||
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
|
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
|
||||||
<relationship name="muted" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="mutedBy" inverseEntity="Toot"/>
|
<relationship name="muted" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="mutedBy" inverseEntity="Toot"/>
|
||||||
|
@ -106,10 +106,10 @@
|
||||||
<attribute name="visibility" optional="YES" attributeType="String"/>
|
<attribute name="visibility" optional="YES" attributeType="String"/>
|
||||||
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="toots" inverseEntity="Application"/>
|
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="toots" inverseEntity="Application"/>
|
||||||
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="toots" inverseEntity="MastodonUser"/>
|
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="toots" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="bookmarked" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
<relationship name="bookmarkedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="toot" inverseEntity="Emoji"/>
|
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="toot" inverseEntity="Emoji"/>
|
||||||
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
|
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="toots" inverseEntity="HomeTimelineIndex"/>
|
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="toot" inverseEntity="HomeTimelineIndex"/>
|
||||||
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
|
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
|
||||||
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
|
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedToot" inverseEntity="MastodonUser"/>
|
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedToot" inverseEntity="MastodonUser"/>
|
||||||
|
@ -127,6 +127,6 @@
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
||||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||||
<element name="Toot" positionX="0" positionY="0" width="128" height="494"/>
|
<element name="Toot" positionX="0" positionY="0" width="128" height="509"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -38,7 +38,7 @@ extension HomeTimelineIndex {
|
||||||
|
|
||||||
index.identifier = property.identifier
|
index.identifier = property.identifier
|
||||||
index.domain = property.domain
|
index.domain = property.domain
|
||||||
index.userID = toot.author.id
|
index.userID = property.userID
|
||||||
index.createdAt = toot.createdAt
|
index.createdAt = toot.createdAt
|
||||||
|
|
||||||
index.toot = toot
|
index.toot = toot
|
||||||
|
@ -63,10 +63,12 @@ extension HomeTimelineIndex {
|
||||||
public struct Property {
|
public struct Property {
|
||||||
public let identifier: String
|
public let identifier: String
|
||||||
public let domain: String
|
public let domain: String
|
||||||
|
public let userID: String
|
||||||
public init(domain: String) {
|
|
||||||
|
public init(domain: String,userID: String) {
|
||||||
self.identifier = UUID().uuidString + "@" + domain
|
self.identifier = UUID().uuidString + "@" + domain
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
|
self.userID = userID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,4 +78,15 @@ extension HomeTimelineIndex: Managed {
|
||||||
return [NSSortDescriptor(keyPath: \HomeTimelineIndex.createdAt, ascending: false)]
|
return [NSSortDescriptor(keyPath: \HomeTimelineIndex.createdAt, ascending: false)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
extension HomeTimelineIndex {
|
||||||
|
|
||||||
|
public static func predicate(userID: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(HomeTimelineIndex.userID), userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static func notDeleted() -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == nil", #keyPath(HomeTimelineIndex.deletedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -39,10 +39,10 @@ public final class Toot: NSManagedObject {
|
||||||
// many-to-one relastionship
|
// many-to-one relastionship
|
||||||
@NSManaged public private(set) var author: MastodonUser
|
@NSManaged public private(set) var author: MastodonUser
|
||||||
@NSManaged public private(set) var reblog: Toot?
|
@NSManaged public private(set) var reblog: Toot?
|
||||||
@NSManaged public private(set) var favouritedBy: MastodonUser?
|
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var rebloggedBy: MastodonUser?
|
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var mutedBy: MastodonUser?
|
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var bookmarkedBy: MastodonUser?
|
@NSManaged public private(set) var bookmarkedBy: Set<MastodonUser>?
|
||||||
|
|
||||||
// one-to-one relastionship
|
// one-to-one relastionship
|
||||||
@NSManaged public private(set) var pinnedBy: MastodonUser?
|
@NSManaged public private(set) var pinnedBy: MastodonUser?
|
||||||
|
@ -104,6 +104,8 @@ public extension Toot {
|
||||||
toot.author = author
|
toot.author = author
|
||||||
toot.reblog = reblog
|
toot.reblog = reblog
|
||||||
|
|
||||||
|
toot.pinnedBy = pinnedBy
|
||||||
|
|
||||||
if let mentions = mentions {
|
if let mentions = mentions {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
|
toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
|
||||||
}
|
}
|
||||||
|
@ -125,9 +127,6 @@ public extension Toot {
|
||||||
if let bookmarkedBy = bookmarkedBy {
|
if let bookmarkedBy = bookmarkedBy {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
|
toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
|
||||||
}
|
}
|
||||||
if let pinnedBy = pinnedBy {
|
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.pinnedBy)).add(pinnedBy)
|
|
||||||
}
|
|
||||||
|
|
||||||
toot.updatedAt = property.networkDate
|
toot.updatedAt = property.networkDate
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,14 @@
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
||||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; };
|
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; };
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; };
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; };
|
||||||
|
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */; };
|
||||||
|
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */; };
|
||||||
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1E425CD46C100561493 /* HomeTimelineViewModel.swift */; };
|
||||||
|
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */; };
|
||||||
|
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */; };
|
||||||
|
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; };
|
||||||
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; };
|
||||||
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
||||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; };
|
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; };
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
||||||
|
@ -23,6 +31,11 @@
|
||||||
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; };
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; };
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
|
||||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; };
|
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; };
|
||||||
|
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; };
|
||||||
|
2D5A3D1125CF87AA002347D6 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D1025CF87AA002347D6 /* AvatarBarButtonItem.swift */; };
|
||||||
|
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; };
|
||||||
|
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; };
|
||||||
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; };
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; };
|
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; };
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
||||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
||||||
|
@ -55,7 +68,6 @@
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; };
|
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; };
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||||
DB0AC70A25CD2E0300D75117 /* HomeViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */; };
|
|
||||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||||
|
@ -97,7 +109,6 @@
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */; };
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */; };
|
||||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54325C13647002E6C99 /* NeedsDependency.swift */; };
|
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54325C13647002E6C99 /* NeedsDependency.swift */; };
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; };
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; };
|
||||||
DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55625C137A8002E6C99 /* HomeViewController.swift */; };
|
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
|
||||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
|
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
|
||||||
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */; };
|
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */; };
|
||||||
|
@ -107,7 +118,6 @@
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
|
||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
|
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
|
||||||
DBD4ED1125CC0FEB0041B741 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */; };
|
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
@ -172,6 +182,14 @@
|
||||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
||||||
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
||||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = "<group>"; };
|
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
|
2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||||
|
2D38F1E425CD46C100561493 /* HomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadLatestState.swift"; sourceTree = "<group>"; };
|
||||||
|
2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
||||||
|
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
||||||
|
2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = "<group>"; };
|
||||||
|
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = "<group>"; };
|
||||||
2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = "<group>"; };
|
2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = "<group>"; };
|
||||||
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
||||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -179,6 +197,11 @@
|
||||||
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = "<group>"; };
|
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
|
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
|
||||||
2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = "<group>"; };
|
2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = "<group>"; };
|
||||||
|
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = "<group>"; };
|
||||||
|
2D5A3D1025CF87AA002347D6 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||||
|
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = "<group>"; };
|
||||||
|
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
||||||
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
||||||
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
||||||
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
|
@ -215,7 +238,6 @@
|
||||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||||
DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = "<group>"; };
|
DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = "<group>"; };
|
||||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||||
DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
|
||||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -264,7 +286,6 @@
|
||||||
DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneCoordinator.swift; sourceTree = "<group>"; };
|
DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneCoordinator.swift; sourceTree = "<group>"; };
|
||||||
DB8AF54325C13647002E6C99 /* NeedsDependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeedsDependency.swift; sourceTree = "<group>"; };
|
DB8AF54325C13647002E6C99 /* NeedsDependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeedsDependency.swift; sourceTree = "<group>"; };
|
||||||
DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = "<group>"; };
|
DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = "<group>"; };
|
||||||
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
|
|
||||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
|
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
|
||||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
||||||
DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -274,7 +295,6 @@
|
||||||
DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||||
DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
||||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
||||||
DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
||||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
||||||
|
@ -354,6 +374,29 @@
|
||||||
path = Content;
|
path = Content;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2D38F1D325CD463600561493 /* HomeTimeline */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */,
|
||||||
|
2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */,
|
||||||
|
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */,
|
||||||
|
2D38F1E425CD46C100561493 /* HomeTimelineViewModel.swift */,
|
||||||
|
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */,
|
||||||
|
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */,
|
||||||
|
2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */,
|
||||||
|
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */,
|
||||||
|
);
|
||||||
|
path = HomeTimeline;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
2D38F1FC25CD47D900561493 /* StatusProvider */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D38F1FD25CD481700561493 /* StatusProvider.swift */,
|
||||||
|
);
|
||||||
|
path = StatusProvider;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2D42FF7C25C82207004A627A /* ToolBar */ = {
|
2D42FF7C25C82207004A627A /* ToolBar */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -367,10 +410,19 @@
|
||||||
children = (
|
children = (
|
||||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */,
|
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */,
|
||||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
|
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
|
||||||
|
2D5A3D1025CF87AA002347D6 /* AvatarBarButtonItem.swift */,
|
||||||
);
|
);
|
||||||
path = Button;
|
path = Button;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2D5A3D0125CF8640002347D6 /* Vender */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
|
||||||
|
);
|
||||||
|
path = Vender;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2D61335525C1886800CAE157 /* Service */ = {
|
2D61335525C1886800CAE157 /* Service */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -392,8 +444,11 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
|
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
|
||||||
|
2D38F1FC25CD47D900561493 /* StatusProvider */,
|
||||||
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
||||||
|
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||||
|
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */,
|
||||||
);
|
);
|
||||||
path = Protocol;
|
path = Protocol;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -561,6 +616,7 @@
|
||||||
children = (
|
children = (
|
||||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||||
|
2D5A3D0125CF8640002347D6 /* Vender */,
|
||||||
2D76319C25C151DE00929FB9 /* Diffiable */,
|
2D76319C25C151DE00929FB9 /* Diffiable */,
|
||||||
DB8AF52A25C13561002E6C99 /* State */,
|
DB8AF52A25C13561002E6C99 /* State */,
|
||||||
2D61335525C1886800CAE157 /* Service */,
|
2D61335525C1886800CAE157 /* Service */,
|
||||||
|
@ -718,6 +774,7 @@
|
||||||
DB8AF55525C1379F002E6C99 /* Scene */ = {
|
DB8AF55525C1379F002E6C99 /* Scene */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
2D38F1D325CD463600561493 /* HomeTimeline */,
|
||||||
2D7631A425C1532200929FB9 /* Share */,
|
2D7631A425C1532200929FB9 /* Share */,
|
||||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||||
DB01409B25C40BB600F9F3CF /* Authentication */,
|
DB01409B25C40BB600F9F3CF /* Authentication */,
|
||||||
|
@ -766,9 +823,6 @@
|
||||||
DBD4ED0B25CC0FD40041B741 /* HomeTimeline */ = {
|
DBD4ED0B25CC0FD40041B741 /* HomeTimeline */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */,
|
|
||||||
DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */,
|
|
||||||
DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */,
|
|
||||||
);
|
);
|
||||||
path = HomeTimeline;
|
path = HomeTimeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1133,7 +1187,10 @@
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||||
|
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
|
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||||
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
||||||
|
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||||
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */,
|
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */,
|
||||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||||
|
@ -1142,15 +1199,18 @@
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
||||||
|
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||||
DB0AC70A25CD2E0300D75117 /* HomeViewController+DebugAction.swift in Sources */,
|
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
|
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||||
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||||
|
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||||
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
||||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
||||||
|
@ -1164,26 +1224,27 @@
|
||||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
||||||
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
||||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
|
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
|
||||||
DBD4ED1125CC0FEB0041B741 /* HomeViewModel.swift in Sources */,
|
|
||||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||||
|
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
||||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||||
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
|
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
|
||||||
|
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
||||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||||
DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */,
|
|
||||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||||
2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */,
|
2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */,
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||||
|
@ -1193,6 +1254,9 @@
|
||||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
|
2D5A3D1125CF87AA002347D6 /* AvatarBarButtonItem.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -12,22 +12,49 @@ import MastodonSDK
|
||||||
|
|
||||||
/// Note: update Equatable when change case
|
/// Note: update Equatable when change case
|
||||||
enum Item {
|
enum Item {
|
||||||
|
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: Attribute)
|
||||||
|
|
||||||
// normal list
|
// normal list
|
||||||
case toot(objectID: NSManagedObjectID)
|
case toot(objectID: NSManagedObjectID)
|
||||||
|
|
||||||
// loader
|
// loader
|
||||||
case middleLoader(tootID: String)
|
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
||||||
|
case publicMiddleLoader(tootID: String)
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Item {
|
||||||
|
class Attribute: Hashable {
|
||||||
|
var separatorLineStyle: SeparatorLineStyle = .indent
|
||||||
|
|
||||||
|
static func == (lhs: Item.Attribute, rhs: Item.Attribute) -> Bool {
|
||||||
|
return lhs.separatorLineStyle == rhs.separatorLineStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(separatorLineStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SeparatorLineStyle {
|
||||||
|
case indent // alignment to name label
|
||||||
|
case expand // alignment to table view two edges
|
||||||
|
case normal // alignment to readable guideline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Item: Equatable {
|
extension Item: Equatable {
|
||||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
|
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
|
||||||
|
return objectIDLeft == objectIDRight
|
||||||
case (.toot(let objectIDLeft), .toot(let objectIDRight)):
|
case (.toot(let objectIDLeft), .toot(let objectIDRight)):
|
||||||
return objectIDLeft == objectIDRight
|
return objectIDLeft == objectIDRight
|
||||||
case (.bottomLoader, .bottomLoader):
|
case (.bottomLoader, .bottomLoader):
|
||||||
return true
|
return true
|
||||||
case (.middleLoader(let upperLeft), .middleLoader(let upperRight)):
|
case (.publicMiddleLoader(let upperLeft), .publicMiddleLoader(let upperRight)):
|
||||||
|
return upperLeft == upperRight
|
||||||
|
case (.homeMiddleLoader(let upperLeft), .homeMiddleLoader(let upperRight)):
|
||||||
return upperLeft == upperRight
|
return upperLeft == upperRight
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -38,10 +65,15 @@ extension Item: Equatable {
|
||||||
extension Item: Hashable {
|
extension Item: Hashable {
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .homeTimelineIndex(let objectID, _):
|
||||||
|
hasher.combine(objectID)
|
||||||
case .toot(let objectID):
|
case .toot(let objectID):
|
||||||
hasher.combine(objectID)
|
hasher.combine(objectID)
|
||||||
case .middleLoader(let upper):
|
case .publicMiddleLoader(let upper):
|
||||||
hasher.combine(String(describing: Item.middleLoader.self))
|
hasher.combine(String(describing: Item.publicMiddleLoader.self))
|
||||||
|
hasher.combine(upper)
|
||||||
|
case .homeMiddleLoader(upperTimelineIndexAnchorObjectID: let upper):
|
||||||
|
hasher.combine(String(describing: Item.homeMiddleLoader.self))
|
||||||
hasher.combine(upper)
|
hasher.combine(upper)
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
hasher.combine(String(describing: Item.bottomLoader.self))
|
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||||
|
|
|
@ -28,6 +28,16 @@ extension TimelineSection {
|
||||||
guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() }
|
guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() }
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
|
case .homeTimelineIndex(objectID: let objectID, attribute: _):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelinePostTableViewCell.self), for: indexPath) as! TimelinePostTableViewCell
|
||||||
|
|
||||||
|
// configure cell
|
||||||
|
managedObjectContext.performAndWait {
|
||||||
|
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
|
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot)
|
||||||
|
}
|
||||||
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
|
return cell
|
||||||
case .toot(let objectID):
|
case .toot(let objectID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelinePostTableViewCell.self), for: indexPath) as! TimelinePostTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelinePostTableViewCell.self), for: indexPath) as! TimelinePostTableViewCell
|
||||||
|
|
||||||
|
@ -38,10 +48,15 @@ extension TimelineSection {
|
||||||
}
|
}
|
||||||
cell.delegate = timelinePostTableViewCellDelegate
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
case .middleLoader(let upperTimelineTootID):
|
case .publicMiddleLoader(let upperTimelineTootID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
||||||
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
||||||
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: upperTimelineTootID)
|
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: upperTimelineTootID,timelineIndexobjectID: nil)
|
||||||
|
return cell
|
||||||
|
case .homeMiddleLoader(let upperTimelineIndexObjectID):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
||||||
|
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: nil,timelineIndexobjectID: upperTimelineIndexObjectID)
|
||||||
return cell
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// DisposeBagCollectable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
protocol DisposeBagCollectable: class {
|
||||||
|
var disposeBag: Set<AnyCancellable> { get set }
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// ScrollViewContainer.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/7.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ScrollViewContainer: UIViewController {
|
||||||
|
var scrollView: UIScrollView { get }
|
||||||
|
func scrollToTop(animated: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ScrollViewContainer {
|
||||||
|
func scrollToTop(animated: Bool) {
|
||||||
|
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// StatusProvider.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
|
||||||
|
func toot() -> Future<Toot?, Never>
|
||||||
|
func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never>
|
||||||
|
func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never>
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// HomeViewController+DebugAction.swift
|
// HomeTimelineViewController+DebugAction.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-2-5.
|
// Created by MainasuK Cirno on 2021-2-5.
|
||||||
|
@ -9,7 +9,7 @@ import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
extension HomeViewController {
|
extension HomeTimelineViewController {
|
||||||
var debugMenu: UIMenu {
|
var debugMenu: UIMenu {
|
||||||
let menu = UIMenu(
|
let menu = UIMenu(
|
||||||
title: "Debug Tools",
|
title: "Debug Tools",
|
||||||
|
@ -27,7 +27,7 @@ extension HomeViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HomeViewController {
|
extension HomeTimelineViewController {
|
||||||
|
|
||||||
@objc private func signOutAction(_ sender: UIAction) {
|
@objc private func signOutAction(_ sender: UIAction) {
|
||||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
|
@ -0,0 +1,50 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineViewController+StatusProvider.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
// MARK: - StatusProvider
|
||||||
|
extension HomeTimelineViewController: StatusProvider {
|
||||||
|
|
||||||
|
func toot() -> Future<Toot?, Never> {
|
||||||
|
return Future { promise in promise(.success(nil)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never> {
|
||||||
|
return Future { promise in
|
||||||
|
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||||
|
assertionFailure()
|
||||||
|
promise(.success(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||||
|
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
promise(.success(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .homeTimelineIndex(let objectID, _):
|
||||||
|
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
|
||||||
|
managedObjectContext.perform {
|
||||||
|
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
|
||||||
|
promise(.success(timelineIndex?.toot))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
promise(.success(nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never> {
|
||||||
|
return Future { promise in promise(.success(nil)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,299 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
|
final class HomeTimelineViewController: UIViewController, NeedsDependency,TimelinePostTableViewCellDelegate {
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
private(set) lazy var viewModel = HomeTimelineViewModel(context: context)
|
||||||
|
|
||||||
|
let avatarBarButtonItem = AvatarBarButtonItem()
|
||||||
|
|
||||||
|
let tableView: UITableView = {
|
||||||
|
let tableView = ControlContainableTableView()
|
||||||
|
tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self))
|
||||||
|
tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
|
||||||
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
title = "Home"
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
navigationItem.leftBarButtonItem = avatarBarButtonItem
|
||||||
|
avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(HomeTimelineViewController.avatarButtonPressed(_:)), for: .touchUpInside)
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
viewModel.tableView = tableView
|
||||||
|
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.setupDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: self,
|
||||||
|
timelinePostTableViewCellDelegate: self,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: self
|
||||||
|
)
|
||||||
|
|
||||||
|
// bind refresh control
|
||||||
|
viewModel.isFetchingLatestTimeline
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isFetching in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !isFetching {
|
||||||
|
UIView.animate(withDuration: 0.5) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
avatarBarButtonItem.avatarButton.menu = debugMenu
|
||||||
|
avatarBarButtonItem.avatarButton.showsMenuAsPrimaryAction = true
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
context.authenticationService.activeMastodonAuthentication.eraseToAnyPublisher(),
|
||||||
|
viewModel.viewDidAppear.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] activeMastodonAuthentication, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let user = activeMastodonAuthentication?.user,
|
||||||
|
let avatarImageURL = user.avatarImageURL() else {
|
||||||
|
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: nil)
|
||||||
|
self.avatarBarButtonItem.configure(withConfigurationInput: input)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: avatarImageURL)
|
||||||
|
self.avatarBarButtonItem.configure(withConfigurationInput: input)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
viewModel.viewDidAppear.send()
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 {
|
||||||
|
self.viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
|
super.viewWillTransition(to: size, with: coordinator)
|
||||||
|
|
||||||
|
coordinator.animate { _ in
|
||||||
|
// do nothing
|
||||||
|
} completion: { _ in
|
||||||
|
// fix AutoLayout cell height not update after rotate issue
|
||||||
|
self.viewModel.cellFrameCache.removeAllObjects()
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineViewController {
|
||||||
|
|
||||||
|
@objc private func avatarButtonPressed(_ sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
guard viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) else {
|
||||||
|
sender.endRefreshing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIScrollViewDelegate
|
||||||
|
extension HomeTimelineViewController {
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard scrollView === tableView else { return }
|
||||||
|
let cells = tableView.visibleCells.compactMap { $0 as? TimelineBottomLoaderTableViewCell }
|
||||||
|
guard let loaderTableViewCell = cells.first else { return }
|
||||||
|
|
||||||
|
if let tabBar = tabBarController?.tabBar, let window = view.window {
|
||||||
|
let loaderTableViewCellFrameInWindow = tableView.convert(loaderTableViewCell.frame, to: nil)
|
||||||
|
let windowHeight = window.frame.height
|
||||||
|
let loaderAppear = (loaderTableViewCellFrameInWindow.origin.y + 0.8 * loaderTableViewCell.frame.height) < (windowHeight - tabBar.frame.height)
|
||||||
|
if loaderAppear {
|
||||||
|
viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension HomeTimelineViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
||||||
|
|
||||||
|
guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
|
||||||
|
return 200
|
||||||
|
}
|
||||||
|
// os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription)
|
||||||
|
|
||||||
|
return ceil(frame.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||||
|
extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
|
||||||
|
func navigationBar() -> UINavigationBar? {
|
||||||
|
return navigationController?.navigationBar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
||||||
|
extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
||||||
|
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID: NSManagedObjectID?) {
|
||||||
|
guard let upperTimelineIndexObjectID = timelineIndexobjectID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModel.loadMiddleSateMachineList
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] ids in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let stateMachine = ids[upperTimelineIndexObjectID] {
|
||||||
|
guard let state = stateMachine.currentState else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make success state same as loading due to snapshot updating delay
|
||||||
|
let isLoading = state is HomeTimelineViewModel.LoadMiddleState.Loading || state is HomeTimelineViewModel.LoadMiddleState.Success
|
||||||
|
cell.loadMoreButton.isHidden = isLoading
|
||||||
|
if isLoading {
|
||||||
|
cell.activityIndicatorView.startAnimating()
|
||||||
|
} else {
|
||||||
|
cell.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cell.loadMoreButton.isHidden = false
|
||||||
|
cell.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
|
var dict = viewModel.loadMiddleSateMachineList.value
|
||||||
|
if let _ = dict[upperTimelineIndexObjectID] {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
HomeTimelineViewModel.LoadMiddleState.Initial(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID),
|
||||||
|
HomeTimelineViewModel.LoadMiddleState.Loading(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID),
|
||||||
|
HomeTimelineViewModel.LoadMiddleState.Fail(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID),
|
||||||
|
HomeTimelineViewModel.LoadMiddleState.Success(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID),
|
||||||
|
])
|
||||||
|
stateMachine.enter(HomeTimelineViewModel.LoadMiddleState.Initial.self)
|
||||||
|
dict[upperTimelineIndexObjectID] = stateMachine
|
||||||
|
viewModel.loadMiddleSateMachineList.value = dict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .homeMiddleLoader(let upper):
|
||||||
|
guard let stateMachine = viewModel.loadMiddleSateMachineList.value[upper] else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stateMachine.enter(HomeTimelineViewModel.LoadMiddleState.Loading.self)
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ScrollViewContainer
|
||||||
|
extension HomeTimelineViewController: ScrollViewContainer {
|
||||||
|
|
||||||
|
var scrollView: UIScrollView { return tableView }
|
||||||
|
|
||||||
|
func scrollToTop(animated: Bool) {
|
||||||
|
if scrollView.contentOffset.y < scrollView.frame.height,
|
||||||
|
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
|
||||||
|
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,
|
||||||
|
!refreshControl.isRefreshing {
|
||||||
|
scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -refreshControl.frame.height), size: CGSize(width: 1, height: 1)), animated: animated)
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.beginRefreshing()
|
||||||
|
self.refreshControl.sendActions(for: .valueChanged)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let indexPath = IndexPath(row: 0, section: 0)
|
||||||
|
guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return }
|
||||||
|
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineViewModel+Diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/7.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
extension HomeTimelineViewModel {
|
||||||
|
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
dependency: NeedsDependency,
|
||||||
|
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate
|
||||||
|
) {
|
||||||
|
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.share()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
diffableDataSource = TimelineSection.tableViewDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: dependency,
|
||||||
|
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
|
|
||||||
|
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
guard let tableView = self.tableView else { return }
|
||||||
|
guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return }
|
||||||
|
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
|
|
||||||
|
let predicate = fetchedResultsController.fetchRequest.predicate
|
||||||
|
let parentManagedObjectContext = fetchedResultsController.managedObjectContext
|
||||||
|
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||||
|
managedObjectContext.parent = parentManagedObjectContext
|
||||||
|
|
||||||
|
managedObjectContext.perform {
|
||||||
|
var shouldAddBottomLoader = false
|
||||||
|
|
||||||
|
let timelineIndexes: [HomeTimelineIndex] = {
|
||||||
|
let request = HomeTimelineIndex.sortedFetchRequest
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
request.predicate = predicate
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request)
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// that's will be the most fastest fetch because of upstream just update and no modify needs consider
|
||||||
|
|
||||||
|
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.Attribute] = [:]
|
||||||
|
|
||||||
|
for item in oldSnapshot.itemIdentifiers {
|
||||||
|
guard case let .homeTimelineIndex(objectID, attribute) = item else { continue }
|
||||||
|
oldSnapshotAttributeDict[objectID] = attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
var newTimelineItems: [Item] = []
|
||||||
|
|
||||||
|
for (i, timelineIndex) in timelineIndexes.enumerated() {
|
||||||
|
let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.Attribute()
|
||||||
|
|
||||||
|
// append new item into snapshot
|
||||||
|
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
||||||
|
|
||||||
|
let isLast = i == timelineIndexes.count - 1
|
||||||
|
switch (isLast, timelineIndex.hasMore) {
|
||||||
|
case (true, false):
|
||||||
|
attribute.separatorLineStyle = .normal
|
||||||
|
case (false, true):
|
||||||
|
attribute.separatorLineStyle = .expand
|
||||||
|
newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID))
|
||||||
|
case (true, true):
|
||||||
|
attribute.separatorLineStyle = .normal
|
||||||
|
shouldAddBottomLoader = true
|
||||||
|
case (false, false):
|
||||||
|
attribute.separatorLineStyle = .indent
|
||||||
|
}
|
||||||
|
} // end for
|
||||||
|
|
||||||
|
var newSnapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>()
|
||||||
|
newSnapshot.appendSections([.main])
|
||||||
|
newSnapshot.appendItems(newTimelineItems, toSection: .main)
|
||||||
|
|
||||||
|
let endSnapshot = CACurrentMediaTime()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||||
|
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
|
||||||
|
diffableDataSource.apply(newSnapshot)
|
||||||
|
self.isFetchingLatestTimeline.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diffableDataSource.apply(newSnapshot, animatingDifferences: false) {
|
||||||
|
tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false)
|
||||||
|
tableView.contentOffset.y = tableView.contentOffset.y - difference.offset
|
||||||
|
self.isFetchingLatestTimeline.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = CACurrentMediaTime()
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: calculate home timeline layout cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - endSnapshot)
|
||||||
|
}
|
||||||
|
} // end perform
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Difference<T> {
|
||||||
|
let item: T
|
||||||
|
let sourceIndexPath: IndexPath
|
||||||
|
let targetIndexPath: IndexPath
|
||||||
|
let offset: CGFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateReloadSnapshotDifference<T: Hashable>(
|
||||||
|
navigationBar: UINavigationBar,
|
||||||
|
tableView: UITableView,
|
||||||
|
oldSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T>,
|
||||||
|
newSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T>
|
||||||
|
) -> Difference<T>? {
|
||||||
|
guard oldSnapshot.numberOfItems != 0 else { return nil }
|
||||||
|
|
||||||
|
// old snapshot not empty. set source index path to first item if not match
|
||||||
|
let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0)
|
||||||
|
|
||||||
|
guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil }
|
||||||
|
|
||||||
|
let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row]
|
||||||
|
guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil }
|
||||||
|
let targetIndexPath = IndexPath(row: itemIndex, section: 0)
|
||||||
|
|
||||||
|
let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar)
|
||||||
|
return Difference(
|
||||||
|
item: timelineItem,
|
||||||
|
sourceIndexPath: sourceIndexPath,
|
||||||
|
targetIndexPath: targetIndexPath,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineViewModel+LoadLatestState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import func QuartzCore.CACurrentMediaTime
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
|
extension HomeTimelineViewModel {
|
||||||
|
class LoadLatestState: GKState {
|
||||||
|
weak var viewModel: HomeTimelineViewModel?
|
||||||
|
|
||||||
|
init(viewModel: HomeTimelineViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
viewModel?.loadLatestStateMachinePublisher.send(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineViewModel.LoadLatestState {
|
||||||
|
class Initial: HomeTimelineViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: HomeTimelineViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Fail.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
assertionFailure()
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let predicate = viewModel.fetchedResultsController.fetchRequest.predicate
|
||||||
|
let parentManagedObjectContext = viewModel.fetchedResultsController.managedObjectContext
|
||||||
|
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||||
|
managedObjectContext.parent = parentManagedObjectContext
|
||||||
|
|
||||||
|
managedObjectContext.perform {
|
||||||
|
let start = CACurrentMediaTime()
|
||||||
|
let latestTootIDs: [Toot.ID]
|
||||||
|
let request = HomeTimelineIndex.sortedFetchRequest
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
request.predicate = predicate
|
||||||
|
|
||||||
|
do {
|
||||||
|
let timelineIndexes = try managedObjectContext.fetch(request)
|
||||||
|
let endFetch = CACurrentMediaTime()
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect timelineIndexes cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, endFetch - start)
|
||||||
|
latestTootIDs = timelineIndexes
|
||||||
|
.prefix(200) // avoid performance issue
|
||||||
|
.compactMap { timelineIndex in
|
||||||
|
timelineIndex.value(forKeyPath: #keyPath(HomeTimelineIndex.toot.id)) as? Toot.ID
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let end = CACurrentMediaTime()
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect toots id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
|
||||||
|
|
||||||
|
// TODO: only set large count when using Wi-Fi
|
||||||
|
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
// TODO: handle error
|
||||||
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
|
||||||
|
} receiveValue: { response in
|
||||||
|
// stop refresher if no new toots
|
||||||
|
let toots = response.value
|
||||||
|
let newToots = toots.filter { !latestTootIDs.contains($0.id) }
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new toots", ((#file as NSString).lastPathComponent), #line, #function, newToots.count)
|
||||||
|
|
||||||
|
if newToots.isEmpty {
|
||||||
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: HomeTimelineViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: HomeTimelineViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineViewModel+LoadMiddleState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
extension HomeTimelineViewModel {
|
||||||
|
class LoadMiddleState: GKState {
|
||||||
|
weak var viewModel: HomeTimelineViewModel?
|
||||||
|
let upperTimelineIndexObjectID: NSManagedObjectID
|
||||||
|
|
||||||
|
init(viewModel: HomeTimelineViewModel, upperTimelineIndexObjectID: NSManagedObjectID) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.upperTimelineIndexObjectID = upperTimelineIndexObjectID
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
var dict = viewModel.loadMiddleSateMachineList.value
|
||||||
|
dict[upperTimelineIndexObjectID] = stateMachine
|
||||||
|
viewModel.loadMiddleSateMachineList.value = dict // trigger value change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineViewModel.LoadMiddleState {
|
||||||
|
|
||||||
|
class Initial: HomeTimelineViewModel.LoadMiddleState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: HomeTimelineViewModel.LoadMiddleState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// guard let viewModel = viewModel else { return false }
|
||||||
|
return stateClass == Success.self || stateClass == Fail.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let timelineIndex = (viewModel.fetchedResultsController.fetchedObjects ?? []).first(where: { $0.objectID == upperTimelineIndexObjectID }) else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let tootIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { timelineIndex in
|
||||||
|
timelineIndex.toot.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: only set large count when using Wi-Fi
|
||||||
|
let maxID = timelineIndex.toot.id
|
||||||
|
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain,maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
|
||||||
|
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
// TODO: handle error
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
let toots = response.value
|
||||||
|
let newToots = toots.filter { !tootIDs.contains($0.id) }
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld toots, %{public}%ld new toots", ((#file as NSString).lastPathComponent), #line, #function, toots.count, newToots.count)
|
||||||
|
if newToots.isEmpty {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
} else {
|
||||||
|
stateMachine.enter(Success.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: HomeTimelineViewModel.LoadMiddleState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// guard let viewModel = viewModel else { return false }
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Success: HomeTimelineViewModel.LoadMiddleState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// guard let viewModel = viewModel else { return false }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineViewModel+LoadOldestState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
|
extension HomeTimelineViewModel {
|
||||||
|
class LoadOldestState: GKState {
|
||||||
|
weak var viewModel: HomeTimelineViewModel?
|
||||||
|
|
||||||
|
init(viewModel: HomeTimelineViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
viewModel?.loadOldestStateMachinePublisher.send(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeTimelineViewModel.LoadOldestState {
|
||||||
|
class Initial: HomeTimelineViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
guard let viewModel = viewModel else { return false }
|
||||||
|
guard !(viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty else { return false }
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: HomeTimelineViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
assertionFailure()
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let last = viewModel.fetchedResultsController.fetchedObjects?.last else {
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: only set large count when using Wi-Fi
|
||||||
|
let maxID = last.toot.id
|
||||||
|
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
|
||||||
|
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
let toots = response.value
|
||||||
|
// enter no more state when no new toots
|
||||||
|
if toots.isEmpty || (toots.count == 1 && toots[0].id == maxID) {
|
||||||
|
stateMachine.enter(NoMore.self)
|
||||||
|
} else {
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: HomeTimelineViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: HomeTimelineViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoMore: HomeTimelineViewModel.LoadOldestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// reset state if needs
|
||||||
|
return stateClass == Idle.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var snapshot = diffableDataSource.snapshot()
|
||||||
|
snapshot.deleteItems([.bottomLoader])
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
//
|
||||||
|
// HomeTimelineViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import func AVFoundation.AVMakeRect
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
import AlamofireImage
|
||||||
|
import DateToolsSwift
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
|
final class HomeTimelineViewModel: NSObject {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||||
|
let fetchedResultsController: NSFetchedResultsController<HomeTimelineIndex>
|
||||||
|
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||||
|
weak var tableView: UITableView?
|
||||||
|
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||||
|
|
||||||
|
// output
|
||||||
|
// top loader
|
||||||
|
private(set) lazy var loadLatestStateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
LoadLatestState.Initial(viewModel: self),
|
||||||
|
LoadLatestState.Loading(viewModel: self),
|
||||||
|
LoadLatestState.Fail(viewModel: self),
|
||||||
|
LoadLatestState.Idle(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(LoadLatestState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
|
||||||
|
// bottom loader
|
||||||
|
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
LoadOldestState.Initial(viewModel: self),
|
||||||
|
LoadOldestState.Loading(viewModel: self),
|
||||||
|
LoadOldestState.Fail(viewModel: self),
|
||||||
|
LoadOldestState.Idle(viewModel: self),
|
||||||
|
LoadOldestState.NoMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(LoadOldestState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
|
||||||
|
// middle loader
|
||||||
|
let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
||||||
|
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
|
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
self.fetchedResultsController = {
|
||||||
|
let fetchRequest = HomeTimelineIndex.sortedFetchRequest
|
||||||
|
fetchRequest.fetchBatchSize = 20
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.toot)]
|
||||||
|
let controller = NSFetchedResultsController(
|
||||||
|
fetchRequest: fetchRequest,
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}()
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
|
timelinePredicate
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.compactMap { $0 }
|
||||||
|
.first() // set once
|
||||||
|
.sink { [weak self] predicate in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.fetchedResultsController.fetchRequest.predicate = predicate
|
||||||
|
do {
|
||||||
|
self.diffableDataSource?.defaultRowAnimation = .fade
|
||||||
|
try self.fetchedResultsController.performFetch()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.diffableDataSource?.defaultRowAnimation = .automatic
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
context.authenticationService.activeMastodonAuthentication
|
||||||
|
.sink { [weak self] activeMastodonAuthentication in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let twitterAuthentication = activeMastodonAuthentication else { return }
|
||||||
|
let activeTwitterUserID = twitterAuthentication.userID
|
||||||
|
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
HomeTimelineIndex.predicate(userID: activeTwitterUserID),
|
||||||
|
HomeTimelineIndex.notDeleted()
|
||||||
|
])
|
||||||
|
self.timelinePredicate.value = predicate
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
//
|
|
||||||
// HomeViewController.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021/1/27.
|
|
||||||
//
|
|
||||||
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class HomeViewController: UIViewController, NeedsDependency {
|
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
|
||||||
|
|
||||||
var viewModel: HomeViewModel!
|
|
||||||
|
|
||||||
let avatarBarButtonItem = AvatarBarButtonItem()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HomeViewController {
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
title = "Home"
|
|
||||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
|
||||||
navigationItem.leftBarButtonItem = avatarBarButtonItem
|
|
||||||
avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(HomeViewController.avatarBarButtonItemDidPressed(_:)), for: .touchUpInside)
|
|
||||||
#if DEBUG
|
|
||||||
avatarBarButtonItem.avatarButton.menu = debugMenu
|
|
||||||
avatarBarButtonItem.avatarButton.showsMenuAsPrimaryAction = true
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
|
||||||
context.authenticationService.activeMastodonAuthentication.eraseToAnyPublisher(),
|
|
||||||
viewModel.viewDidAppear.eraseToAnyPublisher()
|
|
||||||
)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] activeMastodonAuthentication, _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
guard let user = activeMastodonAuthentication?.user,
|
|
||||||
let avatarImageURL = user.avatarImageURL() else {
|
|
||||||
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: nil)
|
|
||||||
self.avatarBarButtonItem.configure(withConfigurationInput: input)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: avatarImageURL)
|
|
||||||
self.avatarBarButtonItem.configure(withConfigurationInput: input)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
viewModel.viewDidAppear.send()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HomeViewController {
|
|
||||||
@objc private func avatarBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
//
|
|
||||||
// HomeViewModel.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-2-4.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class HomeViewModel {
|
|
||||||
|
|
||||||
// input
|
|
||||||
let context: AppContext
|
|
||||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
|
||||||
|
|
||||||
// output
|
|
||||||
|
|
||||||
init(context: AppContext) {
|
|
||||||
self.context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -39,8 +39,7 @@ class MainTabBarController: UITabBarController {
|
||||||
let viewController: UIViewController
|
let viewController: UIViewController
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
let _viewController = HomeViewController()
|
let _viewController = HomeTimelineViewController()
|
||||||
_viewController.viewModel = HomeViewModel(context: context)
|
|
||||||
_viewController.context = context
|
_viewController.context = context
|
||||||
_viewController.coordinator = coordinator
|
_viewController.coordinator = coordinator
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
|
|
@ -142,8 +142,8 @@ extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
|
||||||
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
||||||
extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
||||||
|
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID: NSManagedObjectID?) {
|
||||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String) {
|
guard let upperTimelineTootID = upperTimelineTootID else {return}
|
||||||
viewModel.loadMiddleSateMachineList
|
viewModel.loadMiddleSateMachineList
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] ids in
|
.sink { [weak self] ids in
|
||||||
|
@ -191,7 +191,7 @@ extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegat
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .middleLoader(let upper):
|
case .publicMiddleLoader(let upper):
|
||||||
guard let stateMachine = viewModel.loadMiddleSateMachineList.value[upper] else {
|
guard let stateMachine = viewModel.loadMiddleSateMachineList.value[upper] else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
|
|
|
@ -54,7 +54,7 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
for tuple in indexTootTuples {
|
for tuple in indexTootTuples {
|
||||||
items.append(Item.toot(objectID: tuple.1.objectID))
|
items.append(Item.toot(objectID: tuple.1.objectID))
|
||||||
if tootIDsWhichHasGap.contains(tuple.1.id) {
|
if tootIDsWhichHasGap.contains(tuple.1.id) {
|
||||||
items.append(Item.middleLoader(tootID: tuple.1.id))
|
items.append(Item.publicMiddleLoader(tootID: tuple.1.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ extension PublicTimelineViewModel.LoadMiddleState {
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: fetch tweets failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
|
@ -88,7 +88,7 @@ extension PublicTimelineViewModel.LoadMiddleState {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.tootIDs.value = newTootIDs
|
viewModel.tootIDs.value = newTootIDs
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld tweets, %{public}%ld new tweets", (#file as NSString).lastPathComponent, #line, #function, toots.count, addedToots.count)
|
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld toots, %{public}%ld new toots", (#file as NSString).lastPathComponent, #line, #function, toots.count, addedToots.count)
|
||||||
if addedToots.isEmpty {
|
if addedToots.isEmpty {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol TimelineMiddleLoaderTableViewCellDelegate: class {
|
protocol TimelineMiddleLoaderTableViewCellDelegate: class {
|
||||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String)
|
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?,timelineIndexobjectID:NSManagedObjectID?)
|
||||||
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton)
|
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -421,7 +421,8 @@ extension APIService.Persist {
|
||||||
let timelineIndex = status.homeTimelineIndexes?
|
let timelineIndex = status.homeTimelineIndexes?
|
||||||
.first { $0.userID == requestMastodonUserID }
|
.first { $0.userID == requestMastodonUserID }
|
||||||
if timelineIndex == nil {
|
if timelineIndex == nil {
|
||||||
let timelineIndexProperty = HomeTimelineIndex.Property(domain: domain)
|
let timelineIndexProperty = HomeTimelineIndex.Property(domain: domain,userID: requestMastodonUserID)
|
||||||
|
|
||||||
let _ = HomeTimelineIndex.insert(
|
let _ = HomeTimelineIndex.insert(
|
||||||
into: managedObjectContext,
|
into: managedObjectContext,
|
||||||
property: timelineIndexProperty,
|
property: timelineIndexProperty,
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Ref: https://www.rightpoint.com/rplabs/fixing-controls-and-scrolling-button-views-ios
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// These let you start a touch on a control that's inside a scroll view,
|
||||||
|
// and then if you start dragging, it cancels the touch on the button
|
||||||
|
// and lets you scroll instead. Without these scroll view subclasses,
|
||||||
|
// controls in scroll views will eat touches that start in them, which
|
||||||
|
// prevents scrolling and makes the app feel broken.
|
||||||
|
//
|
||||||
|
// The UITextInput exception is for cases where you have a text field
|
||||||
|
// or a text view in a scroll view. If you press and hold there, you want
|
||||||
|
// to get the text editing magnifier cursor, instead of canceling the
|
||||||
|
// touch in the text input element.
|
||||||
|
//
|
||||||
|
// Ditto for UISlider and UISwitch: if the table view eats the drag gesture,
|
||||||
|
// they feel broken. Feel free to add your own exceptions if you have custom
|
||||||
|
// controls that require swiping or dragging to function.
|
||||||
|
|
||||||
|
final class ControlContainableScrollView: UIScrollView {
|
||||||
|
|
||||||
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||||
|
if view is UIControl
|
||||||
|
&& !(view is UITextInput)
|
||||||
|
&& !(view is UISlider)
|
||||||
|
&& !(view is UISwitch) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.touchesShouldCancel(in: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ControlContainableTableView: UITableView {
|
||||||
|
|
||||||
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||||
|
if view is UIControl
|
||||||
|
&& !(view is UITextInput)
|
||||||
|
&& !(view is UISlider)
|
||||||
|
&& !(view is UISwitch) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.touchesShouldCancel(in: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ControlContainableCollectionView: UICollectionView {
|
||||||
|
|
||||||
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||||
|
if view is UIControl
|
||||||
|
&& !(view is UITextInput)
|
||||||
|
&& !(view is UISlider)
|
||||||
|
&& !(view is UISwitch) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.touchesShouldCancel(in: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue