diff --git a/ios/Approach/Sources/AvatarEditorFeature/AvatarEditor.swift b/ios/Approach/Sources/AvatarEditorFeature/AvatarEditor.swift index 1759c6737..f9e0bf10d 100644 --- a/ios/Approach/Sources/AvatarEditorFeature/AvatarEditor.swift +++ b/ios/Approach/Sources/AvatarEditorFeature/AvatarEditor.swift @@ -24,16 +24,20 @@ public struct AvatarEditor: Reducer { public let isPhotoAvatarsEnabled: Bool public init(avatar: Avatar.Summary?) { + @Dependency(\.featureFlags) var featureFlags + let isPhotoAvatarsEnabled = featureFlags.isFlagEnabled(.photoAvatars) + self.isPhotoAvatarsEnabled = isPhotoAvatarsEnabled + @Dependency(\.uuid) var uuid self.id = avatar?.id ?? uuid() self.initialAvatar = avatar - self.avatarKind = avatar?.kind ?? .text self.text = TextAvatarEditor.State(avatar: avatar) self.photo = PhotoAvatarEditor.State(avatar: avatar) - - @Dependency(\.featureFlags) var featureFlags - let isPhotoAvatarsEnabled = featureFlags.isFlagEnabled(.photoAvatars) - self.isPhotoAvatarsEnabled = isPhotoAvatarsEnabled + if isPhotoAvatarsEnabled { + self.avatarKind = avatar?.kind ?? .text + } else { + self.avatarKind = .text + } } var hasChanges: Bool { @@ -168,7 +172,7 @@ public struct AvatarEditor: Reducer { extension Color { var rgb: Avatar.Background.RGB { let (red, green, blue, _) = UIColor(self).rgba - return .init(red, green, blue) + return Avatar.Background.RGB(red, green, blue) } } diff --git a/ios/Approach/Sources/AvatarEditorFeature/PhotoAvatarEditor.swift b/ios/Approach/Sources/AvatarEditorFeature/PhotoAvatarEditor.swift index d1834759f..c2a7a3554 100644 --- a/ios/Approach/Sources/AvatarEditorFeature/PhotoAvatarEditor.swift +++ b/ios/Approach/Sources/AvatarEditorFeature/PhotoAvatarEditor.swift @@ -14,6 +14,7 @@ public struct PhotoAvatarEditor: Reducer { public struct State: Equatable { public var imageState: ImageState public var photosPickerItem: PhotosPickerItem? + @Presents public var photoCrop: PhotoCrop.State? var value: Avatar.Value? { if let data = imageState.photoData { @@ -43,6 +44,7 @@ public struct PhotoAvatarEditor: Reducer { @CasePathable public enum Internal { case didStartLoadingPhoto case didLoadPhoto(Result) + case photoCrop(PresentationAction) } case view(View) @@ -112,15 +114,28 @@ public struct PhotoAvatarEditor: Reducer { return .none case let .didLoadPhoto(.success(photoData)): - state.imageState = if let photoData { - .success(photoData) + if let photoData { + state.photoCrop = PhotoCrop.State(image: photoData.image) } else { - .empty + state.imageState = .empty } return .none case let .didLoadPhoto(.failure(error)): - state.imageState = .failure(.init(error)) + state.imageState = .failure(AlwaysEqual(error)) + return .none + + case let .photoCrop(.presented(.delegate(.didFinishCropping(image)))): + guard let data = image.pngData() else { + state.imageState = .empty + return .none + } + + state.imageState = .success(PhotoData(data: data, image: image)) + return .none + + case .photoCrop(.dismiss), + .photoCrop(.presented(.view)), .photoCrop(.presented(.binding)), .photoCrop(.presented(.internal)): return .none } @@ -128,11 +143,15 @@ public struct PhotoAvatarEditor: Reducer { return .none } } + .ifLet(\.$photoCrop, action: \.internal.photoCrop) { + PhotoCrop() + } } private func loadTransferable(from imageSelection: PhotosPickerItem) async throws -> PhotoData? { let avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self) guard let avatarImage else { return nil } + try await Task.sleep(for: .seconds(0.5)) return PhotoData(data: avatarImage.data, image: avatarImage.image) } } @@ -154,5 +173,10 @@ public struct PhotoAvatarEditorView: View { photoLibrary: .shared() ) } + .sheet(item: $store.scope(state: \.photoCrop, action: \.internal.photoCrop)) { store in + NavigationStack { + PhotoCropView(store: store) + } + } } } diff --git a/ios/Approach/Sources/AvatarEditorFeature/PhotoCrop.swift b/ios/Approach/Sources/AvatarEditorFeature/PhotoCrop.swift new file mode 100644 index 000000000..0046d4c39 --- /dev/null +++ b/ios/Approach/Sources/AvatarEditorFeature/PhotoCrop.swift @@ -0,0 +1,77 @@ +import ComposableArchitecture +import FeatureActionLibrary +import SwiftUI + +@Reducer +public struct PhotoCrop: Reducer { + @ObservableState + public struct State: Equatable { + public var image: UIImage + public var offset: CGSize = .zero + + public init(image: UIImage) { + self.image = image + } + } + + public enum Action: ViewAction, FeatureAction, BindableAction { + @CasePathable public enum View { + case didTapDone + } + + @CasePathable public enum Delegate { + case didFinishCropping(UIImage) + } + + @CasePathable public enum Internal { + case doNothing + } + + case view(View) + case delegate(Delegate) + case `internal`(Internal) + case binding(BindingAction) + } + + public init() {} + + public var body: some ReducerOf { + BindingReducer() + + Reduce { state, action in + switch action { + case let .view(viewAction): + switch viewAction { + case .didTapDone: + return .none + } + + case let .internal(internalAction): + switch internalAction { + case .doNothing: + return .none + } + + case .delegate, .binding: + return .none + } + } + } +} + +@ViewAction(for: PhotoCrop.self) +public struct PhotoCropView: View { + @Bindable public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + ZStack { + Image(uiImage: store.image) + .resizable() + .scaledToFit() + } + } +}