Skip to content

Commit

Permalink
Leif/p 12 allow customized content (#18)
Browse files Browse the repository at this point in the history
* P-12: Create file for #9

* P-12: Create file for #8

* P-12: Add PictureViewModel and ability to customize

* P-12: Fix indentation
  • Loading branch information
0xLeif authored Oct 17, 2023
1 parent 0965fa2 commit 06531d5
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 12 deletions.
106 changes: 106 additions & 0 deletions Sources/Picture/Internal/MultipleImageView.swift
Original file line number Diff line number Diff line change
@@ -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")
]
)
}
}
40 changes: 40 additions & 0 deletions Sources/Picture/Internal/PictureViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import OSLog
import SwiftUI

class PictureViewModel: ObservableObject {
private let logger = Logger(subsystem: "PictureViewModel", category: "VM")
private var task: Task<Void, Never>?

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)")
}
}
}
}
}
16 changes: 16 additions & 0 deletions Sources/Picture/Internal/SingleImageView.swift
Original file line number Diff line number Diff line change
@@ -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"))
}
}
96 changes: 90 additions & 6 deletions Sources/Picture/Public/Picture+init.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
42 changes: 36 additions & 6 deletions Sources/Picture/Public/Picture.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions Sources/Picture/Public/PictureSourceImage.swift
Original file line number Diff line number Diff line change
@@ -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")))
}
}

0 comments on commit 06531d5

Please sign in to comment.