Skip to content

Commit d5e498f

Browse files
author
Christian Elies
committed
Initial Commit
0 parents  commit d5e498f

File tree

11 files changed

+266
-0
lines changed

11 files changed

+266
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version:5.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "RemoteImage",
8+
platforms: [
9+
.iOS(.v13)
10+
],
11+
products: [
12+
.library(
13+
name: "RemoteImage",
14+
targets: ["RemoteImage"]),
15+
],
16+
targets: [
17+
.target(
18+
name: "RemoteImage",
19+
dependencies: []),
20+
.testTarget(
21+
name: "RemoteImageTests",
22+
dependencies: ["RemoteImage"]),
23+
]
24+
)

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# RemoteImage
2+
3+
[![Swift5](https://img.shields.io/badge/swift5-compatible-green.svg?longCache=true&style=flat-square)](https://developer.apple.com/swift)
4+
[![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg?longCache=true&style=flat-square)](https://www.apple.com/de/ios)
5+
[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg?longCache=true&style=flat-square)](https://en.wikipedia.org/wiki/MIT_License)
6+
7+
This Swift package provides a wrapper view around the existing **SwiftUI** `Image view` which adds support for showing and caching remote images.
8+
In addition you can specify a loading and error view.
9+
10+
## Installation
11+
12+
Add this Swift package in Xcode using its Github repository url. (File > Swift Packages > Add Package Dependency...)
13+
14+
## How to use
15+
16+
Just pass your remote image url and `ViewBuilder`s for the error, image and loading state to the initializer. That's it 🎉
17+
18+
Clear the image cache through `RemoteImageService.cache.removeAllObjects()`.
19+
20+
## Example
21+
22+
The following code truly highlights the **simplicity** of this view:
23+
24+
```swift
25+
let url = URL(string: "https://images.unsplash.com/photo-1524419986249-348e8fa6ad4a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80")!
26+
27+
RemoteImage(url: url, errorView: { error in
28+
Text(error.localizedDescription)
29+
}, imageView: { image in
30+
image
31+
.resizable()
32+
.aspectRatio(contentMode: .fit)
33+
}, loadingView: {
34+
Text("Loading ...")
35+
})
36+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// RemoteImageServiceError.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 11.08.19.
6+
// Copyright © 2019 Christian Elies. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
enum RemoteImageServiceError: Error {
12+
case couldNotCreateImage
13+
}
14+
15+
extension RemoteImageServiceError: LocalizedError {
16+
var errorDescription: String? {
17+
return "Could not create image from received data"
18+
}
19+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// RemoteImageState.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 11.08.19.
6+
// Copyright © 2019 Christian Elies. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
enum RemoteImageState {
12+
case error(_ error: Error)
13+
case image(_ image: UIImage)
14+
case loading
15+
}
16+
17+
extension RemoteImageState: Equatable {
18+
static func == (lhs: RemoteImageState, rhs: RemoteImageState) -> Bool {
19+
switch (lhs, rhs) {
20+
case (.error(let lhsError), .error(let rhsError)):
21+
return (lhsError as NSError) == (rhsError as NSError)
22+
case (.image(let lhsImage), .image(let rhsImage)):
23+
return lhsImage == rhsImage
24+
case (.loading, .loading):
25+
return true
26+
default:
27+
return false
28+
}
29+
}
30+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// RemoteImageService.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 11.08.19.
6+
// Copyright © 2019 Christian Elies. All rights reserved.
7+
//
8+
9+
import Combine
10+
import UIKit
11+
12+
public final class RemoteImageService: ObservableObject {
13+
private var cancellable: AnyCancellable?
14+
15+
var state: RemoteImageState = .loading {
16+
didSet {
17+
objectWillChange.send()
18+
}
19+
}
20+
21+
public static let cache = NSCache<NSURL, UIImage>()
22+
23+
public let objectWillChange = PassthroughSubject<Void, Never>()
24+
25+
func fetchImage(atURL url: URL) {
26+
cancellable?.cancel()
27+
28+
if let image = RemoteImageService.cache.object(forKey: url as NSURL) {
29+
state = .image(image)
30+
return
31+
}
32+
33+
let urlSession = URLSession.shared
34+
let urlRequest = URLRequest(url: url)
35+
36+
cancellable = urlSession.dataTaskPublisher(for: urlRequest)
37+
.map { UIImage(data: $0.data) }
38+
.receive(on: RunLoop.main)
39+
.sink(receiveCompletion: { completion in
40+
switch completion {
41+
case .failure(let failure):
42+
self.state = .error(failure)
43+
default: ()
44+
}
45+
}) { image in
46+
if let image = image {
47+
RemoteImageService.cache.setObject(image, forKey: url as NSURL)
48+
self.state = .image(image)
49+
} else {
50+
self.state = .error(RemoteImageServiceError.couldNotCreateImage)
51+
}
52+
}
53+
}
54+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// RemoteImage.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 11.08.19.
6+
// Copyright © 2019 Christian Elies. All rights reserved.
7+
//
8+
9+
import Combine
10+
import SwiftUI
11+
12+
public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>: View {
13+
private let url: URL
14+
private let errorView: (Error) -> ErrorView
15+
private let imageView: (Image) -> ImageView
16+
private let loadingView: () -> LoadingView
17+
@ObservedObject private var service: RemoteImageService = RemoteImageService()
18+
19+
public var body: AnyView {
20+
switch service.state {
21+
case .error(let error):
22+
return AnyView(
23+
errorView(error)
24+
)
25+
case .image(let image):
26+
return AnyView(
27+
self.imageView(Image(uiImage: image))
28+
)
29+
case .loading:
30+
return AnyView(
31+
loadingView()
32+
.onAppear {
33+
self.service.fetchImage(atURL: self.url)
34+
}
35+
)
36+
}
37+
}
38+
39+
public init(url: URL, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
40+
self.url = url
41+
self.errorView = errorView
42+
self.imageView = imageView
43+
self.loadingView = loadingView
44+
}
45+
}
46+
47+
#if DEBUG
48+
struct RemoteImage_Previews: PreviewProvider {
49+
static var previews: some View {
50+
let url = URL(string: "https://images.unsplash.com/photo-1524419986249-348e8fa6ad4a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80")!
51+
return RemoteImage(url: url, errorView: { error in
52+
Text(error.localizedDescription)
53+
}, imageView: { image in
54+
image
55+
}, loadingView: {
56+
Text("Loading ...")
57+
})
58+
}
59+
}
60+
#endif

Tests/LinuxMain.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import XCTest
2+
3+
import RemoteImageTests
4+
5+
var tests = [XCTestCaseEntry]()
6+
tests += RemoteImageTests.allTests()
7+
XCTMain(tests)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import XCTest
2+
@testable import RemoteImage
3+
4+
final class RemoteImageTests: XCTestCase {
5+
func testExample() {
6+
// This is an example of a functional test case.
7+
// Use XCTAssert and related functions to verify your tests produce the correct
8+
// results.
9+
XCTAssertEqual("Hello, World!", "Hello, World!")
10+
}
11+
12+
static var allTests = [
13+
("testExample", testExample),
14+
]
15+
}

0 commit comments

Comments
 (0)