From 06531d5e347442155c9800a1d3b46de6885a7883 Mon Sep 17 00:00:00 2001 From: Zach Date: Mon, 16 Oct 2023 19:03:19 -0600 Subject: [PATCH] Leif/p 12 allow customized content (#18) * P-12: Create file for #9 * P-12: Create file for #8 * P-12: Add PictureViewModel and ability to customize * P-12: Fix indentation --- .../Picture/Internal/MultipleImageView.swift | 106 ++++++++++++++++++ .../Picture/Internal/PictureViewModel.swift | 40 +++++++ .../Picture/Internal/SingleImageView.swift | 16 +++ Sources/Picture/Public/Picture+init.swift | 96 +++++++++++++++- Sources/Picture/Public/Picture.swift | 42 ++++++- .../Picture/Public/PictureSourceImage.swift | 32 ++++++ 6 files changed, 320 insertions(+), 12 deletions(-) create mode 100644 Sources/Picture/Internal/MultipleImageView.swift create mode 100644 Sources/Picture/Internal/PictureViewModel.swift create mode 100644 Sources/Picture/Internal/SingleImageView.swift create mode 100644 Sources/Picture/Public/PictureSourceImage.swift diff --git a/Sources/Picture/Internal/MultipleImageView.swift b/Sources/Picture/Internal/MultipleImageView.swift new file mode 100644 index 0000000..fd437d9 --- /dev/null +++ b/Sources/Picture/Internal/MultipleImageView.swift @@ -0,0 +1,106 @@ +import SwiftUI + +// TODO: [P-9](https://github.com/0xOpenBytes/Picture/issues/9) +public struct MultipleImageView: View { + let images: [Image] + + public var body: some View { + switch images.count { + case 0: placeholderView + case 1: SingleImageView(image: images[0]) + case 2: doubleImageView + case 3: tripleImageView + default: fallbackImageView + + } + } + + private var placeholderView: some View { + Image(systemName: "photo.artframe") + .resizable() + .aspectRatio(contentMode: .fit) + } + + private var doubleImageView: some View { + VStack { + images[0] + .resizable() + .aspectRatio(contentMode: .fit) + images[1] + .resizable() + .aspectRatio(contentMode: .fit) + } + } + + private var tripleImageView: some View { + HStack { + images[0] + .resizable() + .aspectRatio(contentMode: .fit) + VStack { + images[1] + .resizable() + .aspectRatio(contentMode: .fit) + + images[2] + .resizable() + .aspectRatio(contentMode: .fit) + } + } + } + + private var fallbackImageView: some View { + Grid { + GridRow { + images[0] + .resizable() + .aspectRatio(contentMode: .fit) + images[1] + .resizable() + .aspectRatio(contentMode: .fit) + } + GridRow { + images[2] + .resizable() + .aspectRatio(contentMode: .fit) + Image(systemName: "ellipsis") + .resizable() + .aspectRatio(contentMode: .fit) + } + } + } +} + +struct MultipleImageView_Preview: PreviewProvider { + static var previews: some View { + MultipleImageView( + images: [ + Image(systemName: "photo.artframe") + ] + ) + + MultipleImageView( + images: [ + Image(systemName: "photo.artframe"), + Image(systemName: "photo.artframe") + ] + ) + + MultipleImageView( + images: [ + Image(systemName: "photo.artframe"), + Image(systemName: "photo.artframe"), + Image(systemName: "photo.artframe") + ] + ) + + MultipleImageView( + images: [ + Image(systemName: "photo.artframe"), + Image(systemName: "photo.artframe"), + Image(systemName: "photo.artframe"), + Image(systemName: "photo.artframe") + ] + ) + } +} diff --git a/Sources/Picture/Internal/PictureViewModel.swift b/Sources/Picture/Internal/PictureViewModel.swift new file mode 100644 index 0000000..ea42607 --- /dev/null +++ b/Sources/Picture/Internal/PictureViewModel.swift @@ -0,0 +1,40 @@ +import OSLog +import SwiftUI + +class PictureViewModel: ObservableObject { + private let logger = Logger(subsystem: "PictureViewModel", category: "VM") + private var task: Task? + + let sources: [PictureSource] + + @Published var images: [Image] + + init(sources: [PictureSource]) { + self.sources = sources + self.task = nil + self.images = [] + } + + deinit { + task?.cancel() + } + + @MainActor + func load() { + task?.cancel() + + task = Task { + for source in sources.prefix(5) { + do { + if let loadedImage = try await source.load() { + DispatchQueue.main.async { [weak self] in + self?.images.append(loadedImage) + } + } + } catch { + logger.error("\(error.localizedDescription)") + } + } + } + } +} diff --git a/Sources/Picture/Internal/SingleImageView.swift b/Sources/Picture/Internal/SingleImageView.swift new file mode 100644 index 0000000..f9fb968 --- /dev/null +++ b/Sources/Picture/Internal/SingleImageView.swift @@ -0,0 +1,16 @@ +import SwiftUI + +// TODO: [P-8](https://github.com/0xOpenBytes/Picture/issues/8) +public struct SingleImageView: View { + let image: Image + + public var body: some View { + image + } +} + +struct SingleImageView_Preview: PreviewProvider { + static var previews: some View { + SingleImageView(image: Image(systemName: "photo.artframe")) + } +} diff --git a/Sources/Picture/Public/Picture+init.swift b/Sources/Picture/Public/Picture+init.swift index 2584dee..a6a6486 100644 --- a/Sources/Picture/Public/Picture+init.swift +++ b/Sources/Picture/Public/Picture+init.swift @@ -1,19 +1,103 @@ import SwiftUI -extension Picture { +// MARK: - Default Implementation + +extension Picture +where SingleImageContent == SingleImageView, + MultipleImageContent == MultipleImageView +{ + + public init( + sources: [PictureSource] + ) { + self.init( + sources: sources, + singleImageContent: SingleImageView.init, + multipleImageContent: MultipleImageContent.init + ) + } + public init(image: Image) { - self.init(sources: [.local(image)]) + self.init( + sources: [.local(image)], + singleImageContent: SingleImageView.init, + multipleImageContent: MultipleImageContent.init + ) } public init(url: URL) { - self.init(sources: [.remote(url)]) + self.init( + sources: [.remote(url)], + singleImageContent: SingleImageView.init, + multipleImageContent: MultipleImageContent.init + ) } - + public init(images: [Image]) { - self.init(sources: images.map(PictureSource.local)) + self.init( + sources: images.map(PictureSource.local), + singleImageContent: SingleImageView.init, + multipleImageContent: MultipleImageContent.init + ) } public init(urls: [URL]) { - self.init(sources: urls.map(PictureSource.remote)) + self.init( + sources: urls.map(PictureSource.remote), + singleImageContent: SingleImageView.init, + multipleImageContent: MultipleImageContent.init + ) + } +} + +// MARK: - Single Image Initialization + +extension Picture where MultipleImageContent == MultipleImageView { + public init( + image: Image, + @ViewBuilder singleImageContent: @escaping (Image) -> SingleImageContent + ) { + self.init( + sources: [.local(image)], + singleImageContent: singleImageContent, + multipleImageContent: MultipleImageContent.init + ) + } + + public init( + url: URL, + @ViewBuilder singleImageContent: @escaping (Image) -> SingleImageContent + ) { + self.init( + sources: [.remote(url)], + singleImageContent: singleImageContent, + multipleImageContent: MultipleImageContent.init + ) + } +} + +// MARK: - Multiple Image Initialization + +extension Picture where SingleImageContent == SingleImageView { + public init( + images: [Image], + @ViewBuilder multipleImageContent: @escaping ([Image]) -> MultipleImageContent + ) { + self.init( + sources: images.map(PictureSource.local), + singleImageContent: SingleImageView.init, + multipleImageContent: multipleImageContent + ) + } + + public init( + urls: [URL], + @ViewBuilder multipleImageContent: @escaping ([Image]) -> MultipleImageContent + ) { + self.init( + sources: urls.map(PictureSource.remote), + singleImageContent: SingleImageView.init, + multipleImageContent: multipleImageContent + ) } } diff --git a/Sources/Picture/Public/Picture.swift b/Sources/Picture/Public/Picture.swift index edd42eb..833d452 100644 --- a/Sources/Picture/Public/Picture.swift +++ b/Sources/Picture/Public/Picture.swift @@ -1,14 +1,28 @@ import SwiftUI -public struct Picture: View { +public struct Picture< + SingleImageContent: View, + MultipleImageContent: View +>: View { private let sources: [PictureSource] + private let singleImageContent: (Image) -> SingleImageContent + private let multipleImageContent: ([Image]) -> MultipleImageContent + + @ObservedObject private var viewModel: PictureViewModel @State private var isFullscreen: Bool = false @State private var isViewingImage: Bool = false @State private var currentSourceIndex: Int = 0 - public init(sources: [PictureSource]) { + public init( + sources: [PictureSource], + @ViewBuilder singleImageContent: @escaping (Image) -> SingleImageContent, + @ViewBuilder multipleImageContent: @escaping ([Image]) -> MultipleImageContent + ) { + self._viewModel = ObservedObject(initialValue: PictureViewModel(sources: sources)) self.sources = sources + self.singleImageContent = singleImageContent + self.multipleImageContent = multipleImageContent } @ViewBuilder @@ -70,10 +84,14 @@ public struct Picture: View { public var body: some View { Group { - if sources.count == 1 { - Text("Image") - } else { - Text("Images: \(sources.count)") + switch viewModel.images.count { + case 0: + ProgressView() + .onAppear { + viewModel.load() + } + case 1: singleImageBody + default: multipleImageBody } } .sheet(isPresented: $isFullscreen) { @@ -83,6 +101,18 @@ public struct Picture: View { isFullscreen = true } } + + @ViewBuilder + private var singleImageBody: some View { + if let image = viewModel.images.first { + singleImageContent(image) + } + } + + @ViewBuilder + private var multipleImageBody: some View { + multipleImageContent(viewModel.images) + } } struct Picture_Preview: PreviewProvider { diff --git a/Sources/Picture/Public/PictureSourceImage.swift b/Sources/Picture/Public/PictureSourceImage.swift new file mode 100644 index 0000000..14b781e --- /dev/null +++ b/Sources/Picture/Public/PictureSourceImage.swift @@ -0,0 +1,32 @@ +import SwiftUI + +public struct PictureSourceImage: View { + @ObservedObject private var viewModel: InteractableImageViewModel + + public init( + source: PictureSource + ) { + self._viewModel = ObservedObject( + initialValue: InteractableImageViewModel(source: source) + ) + } + + public var body: some View { + if let image = viewModel.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + } else { + ProgressView() + .onAppear { + viewModel.load() + } + } + } +} + +struct PictureSourceImage_Preview: PreviewProvider { + static var previews: some View { + PictureSourceImage(source: .local(Image(systemName: "photo.artframe"))) + } +}