Added room avatar caching and room list loading state.

This commit is contained in:
Stefan Ceriu 2022-02-22 12:37:44 +02:00
parent ad80b91b16
commit c35438cbb8
11 changed files with 118 additions and 47 deletions

View File

@ -19,6 +19,7 @@
182BC47727C4CD6D00A30C33 /* ActivityDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182BC46E27C4CD6D00A30C33 /* ActivityDismissal.swift */; }; 182BC47727C4CD6D00A30C33 /* ActivityDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182BC46E27C4CD6D00A30C33 /* ActivityDismissal.swift */; };
182BC47927C4CE2200A30C33 /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182BC47827C4CE2200A30C33 /* LabelledActivityIndicatorView.swift */; }; 182BC47927C4CE2200A30C33 /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182BC47827C4CE2200A30C33 /* LabelledActivityIndicatorView.swift */; };
182BC47B27C4D05200A30C33 /* FullscreenLoadingActivityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182BC47A27C4D05200A30C33 /* FullscreenLoadingActivityPresenter.swift */; }; 182BC47B27C4D05200A30C33 /* FullscreenLoadingActivityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182BC47A27C4D05200A30C33 /* FullscreenLoadingActivityPresenter.swift */; };
182BC48127C4EBBB00A30C33 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 182BC48027C4EBBB00A30C33 /* Kingfisher */; };
1850253F27B6918D002E6B18 /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850253E27B6918D002E6B18 /* ElementXTests.swift */; }; 1850253F27B6918D002E6B18 /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850253E27B6918D002E6B18 /* ElementXTests.swift */; };
1850254927B6918D002E6B18 /* ElementXUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850254827B6918D002E6B18 /* ElementXUITests.swift */; }; 1850254927B6918D002E6B18 /* ElementXUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850254827B6918D002E6B18 /* ElementXUITests.swift */; };
1850254B27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850254A27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift */; }; 1850254B27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850254A27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift */; };
@ -154,6 +155,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
182BC48127C4EBBB00A30C33 /* Kingfisher in Frameworks */,
1863A3FC27BA5A9100B52E4D /* KeychainAccess in Frameworks */, 1863A3FC27BA5A9100B52E4D /* KeychainAccess in Frameworks */,
1850255927B69388002E6B18 /* MatrixRustSDK in Frameworks */, 1850255927B69388002E6B18 /* MatrixRustSDK in Frameworks */,
1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */, 1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */,
@ -489,6 +491,7 @@
1850255827B69388002E6B18 /* MatrixRustSDK */, 1850255827B69388002E6B18 /* MatrixRustSDK */,
1863A3FB27BA5A9100B52E4D /* KeychainAccess */, 1863A3FB27BA5A9100B52E4D /* KeychainAccess */,
1863A40527BA6DFC00B52E4D /* SwiftyBeaver */, 1863A40527BA6DFC00B52E4D /* SwiftyBeaver */,
182BC48027C4EBBB00A30C33 /* Kingfisher */,
); );
productName = ElementX; productName = ElementX;
productReference = 1850252427B6918C002E6B18 /* ElementX.app */; productReference = 1850252427B6918C002E6B18 /* ElementX.app */;
@ -566,6 +569,7 @@
1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */, 1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */,
1863A3FA27BA5A9100B52E4D /* XCRemoteSwiftPackageReference "KeychainAccess" */, 1863A3FA27BA5A9100B52E4D /* XCRemoteSwiftPackageReference "KeychainAccess" */,
1863A40427BA6DFC00B52E4D /* XCRemoteSwiftPackageReference "SwiftyBeaver" */, 1863A40427BA6DFC00B52E4D /* XCRemoteSwiftPackageReference "SwiftyBeaver" */,
182BC47F27C4EBBB00A30C33 /* XCRemoteSwiftPackageReference "Kingfisher" */,
); );
productRefGroup = 1850252527B6918C002E6B18 /* Products */; productRefGroup = 1850252527B6918C002E6B18 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -1021,6 +1025,14 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
182BC47F27C4EBBB00A30C33 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 7.0.0;
};
};
1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */ = { 1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift.git"; repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift.git";
@ -1048,6 +1060,11 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
182BC48027C4EBBB00A30C33 /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 182BC47F27C4EBBB00A30C33 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
1850255827B69388002E6B18 /* MatrixRustSDK */ = { 1850255827B69388002E6B18 /* MatrixRustSDK */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; package = 1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */;

View File

@ -11,12 +11,12 @@
} }
}, },
{ {
"package": "MatrixRustSDK", "package": "Kingfisher",
"repositoryURL": "https://github.com/matrix-org/matrix-rust-components-swift.git", "repositoryURL": "https://github.com/onevcat/Kingfisher",
"state": { "state": {
"branch": "main", "branch": null,
"revision": "cb680b1783849ecabd0bdf61f65faff767ce32c8", "revision": "0c02c46cfdc0656ce74fd0963a75e5000a0b7f23",
"version": null "version": "7.1.2"
} }
}, },
{ {

View File

@ -6,6 +6,7 @@
// //
import UIKit import UIKit
import Kingfisher
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private let window: UIWindow private let window: UIWindow
@ -76,7 +77,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
} }
let parameters = HomeScreenCoordinatorParameters(userSession: userSession) let parameters = HomeScreenCoordinatorParameters(userSession: userSession)
let coordinator = HomeScreenCoordinator(parameters: parameters) let coordinator = HomeScreenCoordinator(parameters: parameters, imageCache: ImageCache.default)
coordinator.completion = { [weak self] result in coordinator.completion = { [weak self] result in
switch result { switch result {

View File

@ -50,7 +50,7 @@ class UserSession: ClientDelegate {
} }
} }
func getUserAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) { func loadUserAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
do { do {
let avatarData = try self.client.avatar() let avatarData = try self.client.avatar()

View File

@ -16,6 +16,7 @@
import SwiftUI import SwiftUI
import Combine import Combine
import Kingfisher
struct HomeScreenCoordinatorParameters { struct HomeScreenCoordinatorParameters {
let userSession: UserSession let userSession: UserSession
@ -45,11 +46,11 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
// MARK: - Setup // MARK: - Setup
init(parameters: HomeScreenCoordinatorParameters) { init(parameters: HomeScreenCoordinatorParameters, imageCache: Kingfisher.ImageCache) {
self.parameters = parameters self.parameters = parameters
let userDisplayName = self.parameters.userSession.userDisplayName ?? self.parameters.userSession.userIdentifier let userDisplayName = self.parameters.userSession.userDisplayName ?? self.parameters.userSession.userIdentifier
viewModel = HomeScreenViewModel(userDisplayName: userDisplayName) viewModel = HomeScreenViewModel(userDisplayName: userDisplayName, imageCache: imageCache)
let view = HomeScreen(context: viewModel.context) let view = HomeScreen(context: viewModel.context)
hostingController = UIHostingController(rootView: view) hostingController = UIHostingController(rootView: view)
@ -61,7 +62,7 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
case .logout: case .logout:
self.completion?(.logout) self.completion?(.logout)
case .loadUserAvatar: case .loadUserAvatar:
self.parameters.userSession.getUserAvatar({ result in self.parameters.userSession.loadUserAvatar({ result in
switch result { switch result {
case .success(let avatar): case .success(let avatar):
self.viewModel.updateWithUserAvatar(avatar) self.viewModel.updateWithUserAvatar(avatar)

View File

@ -33,6 +33,7 @@ struct HomeScreenViewState: BindableState {
var userAvatar: UIImage? var userAvatar: UIImage?
var rooms: [HomeScreenRoom] = [] var rooms: [HomeScreenRoom] = []
var isLoadingRooms: Bool = false
var directRooms: [HomeScreenRoom] { var directRooms: [HomeScreenRoom] {
rooms.filter { $0.isDirect } rooms.filter { $0.isDirect }

View File

@ -15,6 +15,7 @@
// //
import SwiftUI import SwiftUI
import Kingfisher
@available(iOS 14, *) @available(iOS 14, *)
typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState, typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState,
@ -27,7 +28,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
// MARK: Private // MARK: Private
private var roomList: [RoomModelProtocol]? private var roomList: [RoomModelProtocol]? {
didSet {
self.state.isLoadingRooms = (roomList == nil)
}
}
private let imageCache: ImageCache
// MARK: Public // MARK: Public
@ -35,8 +41,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
// MARK: - Setup // MARK: - Setup
init(userDisplayName: String) { init(userDisplayName: String, imageCache: Kingfisher.ImageCache) {
super.init(initialViewState: HomeScreenViewState(userDisplayName: userDisplayName)) self.imageCache = imageCache
super.init(initialViewState: HomeScreenViewState(userDisplayName: userDisplayName, isLoadingRooms: true))
} }
// MARK: - Public // MARK: - Public
@ -46,24 +53,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
case .logout: case .logout:
self.completion?(.logout) self.completion?(.logout)
case .loadRoomAvatar(let roomId): case .loadRoomAvatar(let roomId):
guard let room = roomList?.filter({ $0.identifier == roomId }).first else { self.loadAvatarForRoomWithIdentifier(roomId)
break
}
room.getAvatar { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let image):
guard let index = self.state.rooms.firstIndex(where: { $0.id == roomId }) else {
return
}
self.state.rooms[index].avatar = image
default:
break
}
}
case .loadUserAvatar: case .loadUserAvatar:
self.completion?(.loadUserAvatar) self.completion?(.loadUserAvatar)
} }
@ -84,4 +74,51 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
func updateWithUserAvatar(_ avatar: UIImage?) { func updateWithUserAvatar(_ avatar: UIImage?) {
self.state.userAvatar = avatar self.state.userAvatar = avatar
} }
// MARK: - Private
private func loadAvatarForRoomWithIdentifier(_ roomIdentifier: String) {
guard let room = roomList?.filter({ $0.identifier == roomIdentifier }).first,
let cacheKey = room.avatarURL?.path else {
return
}
if imageCache.isCached(forKey: cacheKey) {
imageCache.retrieveImage(forKey: cacheKey) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let value):
self.updateAvatar(value.image, forRoomWithIdentifier: roomIdentifier)
case .failure(let error):
MXLog.error("Failed retrieving avatar from cache with error: \(error)")
}
}
return
}
room.loadAvatar { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let avatar):
guard let avatar = avatar else {
return
}
self.imageCache.store(avatar, forKey: cacheKey)
self.updateAvatar(avatar, forRoomWithIdentifier: roomIdentifier)
default:
break
}
}
}
private func updateAvatar(_ avatar: UIImage?, forRoomWithIdentifier roomIdentifier: String) {
guard let index = self.state.rooms.firstIndex(where: { $0.id == roomIdentifier }) else {
return
}
self.state.rooms[index].avatar = avatar
}
} }

View File

@ -15,6 +15,7 @@
// //
import SwiftUI import SwiftUI
import Kingfisher
struct HomeScreen: View { struct HomeScreen: View {
@ -31,6 +32,7 @@ struct HomeScreen: View {
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: 40, height: 40, alignment: .center) .frame(width: 40, height: 40, alignment: .center)
.mask(Circle())
} else { } else {
let _ = context.send(viewAction: .loadUserAvatar) let _ = context.send(viewAction: .loadUserAvatar)
} }
@ -40,21 +42,30 @@ struct HomeScreen: View {
} }
.padding(.vertical, 32.0) .padding(.vertical, 32.0)
List { if context.viewState.isLoadingRooms {
Section("People") { VStack {
ForEach(context.viewState.directRooms) { room in Text("Loading rooms")
RoomCell(room: room, context: context) ProgressView()
} }
} } else {
List {
Section("Rooms") { Section("People") {
ForEach(context.viewState.nondirectRooms) { room in ForEach(context.viewState.directRooms) { room in
RoomCell(room: room, context: context) RoomCell(room: room, context: context)
}
}
Section("Rooms") {
ForEach(context.viewState.nondirectRooms) { room in
RoomCell(room: room, context: context)
}
} }
} }
.headerProminence(.increased)
.listStyle(.plain)
} }
.headerProminence(.increased)
.listStyle(.plain) Spacer()
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
@ -81,6 +92,7 @@ struct RoomCell: View {
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.mask(Circle())
} else { } else {
let _ = context.send(viewAction: .loadRoomAvatar(roomId: room.id)) let _ = context.send(viewAction: .loadRoomAvatar(roomId: room.id))
Image(systemName: "person.3") Image(systemName: "person.3")
@ -119,7 +131,7 @@ struct RoomCell: View {
struct HomeScreen_Previews: PreviewProvider { struct HomeScreen_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let viewModel = HomeScreenViewModel(userDisplayName: "Johnny Appleseed") let viewModel = HomeScreenViewModel(userDisplayName: "Johnny Appleseed", imageCache: ImageCache.default)
let rooms = [MockRoomModel(displayName: "Alfa"), let rooms = [MockRoomModel(displayName: "Alfa"),
MockRoomModel(displayName: "Beta"), MockRoomModel(displayName: "Beta"),
@ -127,6 +139,8 @@ struct HomeScreen_Previews: PreviewProvider {
viewModel.updateWithRoomList(rooms) viewModel.updateWithRoomList(rooms)
viewModel.updateWithUserAvatar(UIImage(systemName: "person.fill.questionmark"))
return HomeScreen(context: viewModel.context) return HomeScreen(context: viewModel.context)
} }
} }

View File

@ -23,7 +23,7 @@ struct MockRoomModel: RoomModelProtocol {
let isPublic = Bool.random() let isPublic = Bool.random()
let isEncrypted = Bool.random() let isEncrypted = Bool.random()
func getAvatar(_ completion: (Result<UIImage?, Error>) -> Void) { func loadAvatar(_ completion: (Result<UIImage?, Error>) -> Void) {
completion(.success(UIImage(systemName: "wand.and.stars"))) completion(.success(UIImage(systemName: "wand.and.stars")))
} }
} }

View File

@ -74,7 +74,7 @@ struct RoomModel: RoomModelProtocol {
return URL(string: urlString) return URL(string: urlString)
} }
func getAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) { func loadAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
do { do {
let avatarData = try room.avatar() let avatarData = try room.avatar()

View File

@ -22,5 +22,5 @@ protocol RoomModelProtocol {
var avatarURL: URL? { get } var avatarURL: URL? { get }
func getAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) func loadAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void)
} }