├── Albums.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── project.pbxproj ├── LICENSE.md ├── Albums ├── Albums-Bridging-Header.h ├── NetworkSession.swift ├── NetworkDataHandler.swift ├── AlbumsApp.swift ├── NetworkImageSource.h ├── NetworkImageSource.m ├── AlbumsListRowModel.swift ├── NetworkImageSerialization.swift ├── NetworkJSONOperation.swift ├── NetworkImageOperation.swift ├── AlbumsListModel.swift ├── NetworkImageHandler.swift ├── NetworkJSONHandler.swift ├── AlbumsListView.swift └── AlbumsListRowView.swift ├── AlbumsTests ├── AlbumsTests.swift ├── NetworkDataHandlerTests.swift ├── NetworkSessionTests.swift ├── AlbumsListRowModelTests.swift ├── AlbumsListModelTests.swift ├── NetworkImageSerializationTests.swift ├── NetworkJSONOperationTests.swift ├── NetworkImageOperationTests.swift ├── NetworkImageSourceTests.m ├── NetworkImageHandlerTests.swift └── NetworkJSONHandlerTests.swift └── README.md /Albums.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Albums.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | TDD-Albums-II 2 | 3 | Copyright © 2021 North Bronson Software 4 | 5 | This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Albums/Albums-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 North Bronson Software 3 | // 4 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 5 | // 6 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | // 10 | 11 | #import "NetworkImageSource.h" 12 | -------------------------------------------------------------------------------- /Albums/NetworkSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkSession.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol NetworkSessionURLSession { 17 | associatedtype URLSession : NetworkSessionURLSession 18 | 19 | static var shared: URLSession { get } 20 | 21 | func data( 22 | for: URLRequest, 23 | delegate: URLSessionTaskDelegate? 24 | ) async throws -> ( 25 | Data, 26 | URLResponse 27 | ) 28 | } 29 | 30 | extension URLSession : NetworkSessionURLSession { 31 | 32 | } 33 | 34 | struct NetworkSession { 35 | 36 | } 37 | 38 | extension NetworkSession { 39 | static func data(for request: URLRequest) async throws -> ( 40 | Data, 41 | URLResponse 42 | ) { 43 | return try await URLSession.shared.data( 44 | for: request, 45 | delegate: nil 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Albums/NetworkDataHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkDataHandler.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | struct NetworkDataHandler { 17 | 18 | } 19 | 20 | extension NetworkDataHandler { 21 | static func data( 22 | with data: Data, 23 | response: URLResponse 24 | ) throws -> Data { 25 | guard 26 | let statusCode = (response as? HTTPURLResponse)?.statusCode, 27 | 200...299 ~= statusCode 28 | else { 29 | throw Self.Error(.statusCodeError) 30 | } 31 | return data 32 | } 33 | } 34 | 35 | extension NetworkDataHandler { 36 | struct Error : Swift.Error { 37 | enum Code { 38 | case statusCodeError 39 | } 40 | 41 | let code: Self.Code 42 | let underlying: Swift.Error? 43 | 44 | init( 45 | _ code: Self.Code, 46 | underlying: Swift.Error? = nil 47 | ) { 48 | self.code = code 49 | self.underlying = underlying 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Albums/AlbumsApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsApp.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import SwiftUI 15 | 16 | @main struct AlbumsApp: App { 17 | @StateObject private var model = ListModel() 18 | } 19 | 20 | extension AlbumsApp { 21 | private typealias JSONHandler = NetworkJSONHandler 22 | private typealias ImageHandler = NetworkImageHandler> 23 | 24 | private typealias JSONOperation = NetworkJSONOperation, JSONHandler> 25 | private typealias ImageOperation = NetworkImageOperation, ImageHandler> 26 | 27 | private typealias ListModel = AlbumsListModel 28 | private typealias ListRowModel = AlbumsListRowModel 29 | 30 | private typealias ListView = AlbumsListView 31 | } 32 | 33 | extension AlbumsApp { 34 | var body: some Scene { 35 | WindowGroup { 36 | ListView( 37 | model: self.model 38 | ) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /AlbumsTests/AlbumsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | func DataTestDouble() -> Data { 17 | return Data(UInt8.min...UInt8.max) 18 | } 19 | 20 | func HTTPURLResponseTestDouble( 21 | statusCode: Int = 200, 22 | headerFields: Dictionary? = nil 23 | ) -> HTTPURLResponse { 24 | return HTTPURLResponse( 25 | url: URLTestDouble(), 26 | statusCode: statusCode, 27 | httpVersion: "HTTP/1.1", 28 | headerFields: headerFields 29 | )! 30 | } 31 | 32 | func NSErrorTestDouble() -> NSError { 33 | return NSError( 34 | domain: "", 35 | code: 0 36 | ) 37 | } 38 | 39 | func URLRequestTestDouble() -> URLRequest { 40 | return URLRequest(url: URLTestDouble()) 41 | } 42 | 43 | func URLResponseTestDouble() -> URLResponse { 44 | return URLResponse( 45 | url: URLTestDouble(), 46 | mimeType: nil, 47 | expectedContentLength: 0, 48 | textEncodingName: nil 49 | ) 50 | } 51 | 52 | func URLTestDouble() -> URL { 53 | return URL(string: "http://localhost/")! 54 | } 55 | -------------------------------------------------------------------------------- /Albums/NetworkImageSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageSource.h 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | #import 15 | #import 16 | 17 | @interface NetworkImageSource : NSObject 18 | 19 | @end 20 | 21 | @interface NetworkImageSource (CreateImageSource) 22 | 23 | + (CGImageSourceRef _Nullable (*_Nonnull)(CFDataRef _Nonnull, CFDictionaryRef _Nullable))createImageSource; 24 | 25 | + (CGImageSourceRef _Nullable)createImageSourceWithData:(CFDataRef _Nonnull)data 26 | options:(CFDictionaryRef _Nullable)options CF_RETURNS_RETAINED; 27 | 28 | @end 29 | 30 | @interface NetworkImageSource (CreateImage) 31 | 32 | + (CGImageRef _Nullable (*_Nonnull)(CGImageSourceRef _Nonnull, size_t, CFDictionaryRef _Nullable))createImage; 33 | 34 | + (CGImageRef _Nullable)createImageWithImageSource:(CGImageSourceRef _Nonnull)imageSource 35 | atIndex:(size_t)index 36 | options:(CFDictionaryRef _Nullable)options CF_RETURNS_RETAINED; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Albums/NetworkImageSource.m: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageSource.m 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | #import "NetworkImageSource.h" 15 | 16 | @implementation NetworkImageSource 17 | 18 | @end 19 | 20 | @implementation NetworkImageSource (CreateImageSource) 21 | 22 | + (CGImageSourceRef _Nullable (*_Nonnull)(CFDataRef _Nonnull, CFDictionaryRef _Nullable))createImageSource { 23 | return CGImageSourceCreateWithData; 24 | } 25 | 26 | + (CGImageSourceRef)createImageSourceWithData:(CFDataRef)data 27 | options:(CFDictionaryRef)options { 28 | return [self createImageSource](data, options); 29 | } 30 | 31 | @end 32 | 33 | @implementation NetworkImageSource (CreateImage) 34 | 35 | + (CGImageRef _Nullable (*_Nonnull)(CGImageSourceRef _Nonnull, size_t, CFDictionaryRef _Nullable))createImage { 36 | return CGImageSourceCreateImageAtIndex; 37 | } 38 | 39 | + (CGImageRef)createImageWithImageSource:(CGImageSourceRef)imageSource 40 | atIndex:(size_t)index 41 | options:(CFDictionaryRef)options { 42 | return [self createImage](imageSource, index, options); 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TDD-Albums-II 2 | 3 | Welcome! The TDD-Albums-II tutorial is a sequel to the original TDD-Albums library from 2015. The TDD-Albums library started as a hands-on tutorial for a few iPhone developers at eBay that were interested in learning Test-Driven Development. Our previous tutorial was written using Objective-C and UIKit. The TDD-Albums-II tutorial has been rewritten (from scratch) using the latest versions of Swift (5.5) and SwiftUI. 4 | 5 | Like our previous tutorial, we will cover topics familiar to iOS engineers, like networking and concurrency, while working (almost) totally from TDD. Our tutorial also introduces topics familiar to TDD engineers, like dependency-injection and test-double types, while working from Swift, SwiftUI, and a little Objective-C. 6 | 7 | TDD-Albums-II is appropriate for iOS engineers that have never attempted TDD (or never written a unit-test). Having said that, TDD-Albums-II is not intended as a tutorial to learn iOS engineering. It will be assumed that you have a strong competency in Swift and SwiftUI. You should also have a working knowledge of Objective-C. 8 | 9 | Open the [Wiki](https://github.com/vanvoorden/TDD-Albums-II/wiki) for step-by-step instructions. 10 | 11 | ## Requirements 12 | 13 | This project requires Xcode 13.1 or later. 14 | 15 | ## License 16 | TDD-Albums-II 17 | 18 | Copyright © 2021 North Bronson Software 19 | 20 | This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 21 | 22 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /Albums/AlbumsListRowModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsListRowModel.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol AlbumsListRowModelImageOperation { 17 | associatedtype Image 18 | 19 | static func image(for: URLRequest) async throws -> Image 20 | } 21 | 22 | extension NetworkImageOperation : AlbumsListRowModelImageOperation where Session == NetworkSession, ImageHandler == NetworkImageHandler> { 23 | 24 | } 25 | 26 | @MainActor final class AlbumsListRowModel : ObservableObject { 27 | @Published private(set) var image: ImageOperation.Image? 28 | 29 | private let album: Album 30 | 31 | init(album: Album) { 32 | self.album = album 33 | } 34 | } 35 | 36 | extension AlbumsListRowModel { 37 | var artist: String { 38 | return self.album.artist 39 | } 40 | } 41 | 42 | extension AlbumsListRowModel { 43 | var name: String { 44 | return self.album.name 45 | } 46 | } 47 | 48 | extension AlbumsListRowModel { 49 | func requestImage() async throws { 50 | if let url = URL(string: self.album.image) { 51 | let request = URLRequest(url: url) 52 | let image = try await ImageOperation.image(for: request) 53 | self.image = image 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Albums/NetworkImageSerialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageSerialization.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol NetworkImageSerializationImageSource { 17 | associatedtype ImageSource 18 | associatedtype Image 19 | 20 | static func createImageSource( 21 | with: CFData, 22 | options: CFDictionary? 23 | ) -> ImageSource? 24 | 25 | static func createImage( 26 | with: ImageSource, 27 | at: Int, 28 | options: CFDictionary? 29 | ) -> Image? 30 | } 31 | 32 | extension NetworkImageSource : NetworkImageSerializationImageSource { 33 | 34 | } 35 | 36 | struct NetworkImageSerialization { 37 | static func image(with data: Data) throws -> ImageSource.Image { 38 | guard 39 | let imageSource = ImageSource.createImageSource( 40 | with: data as CFData, 41 | options: nil 42 | ) 43 | else { 44 | throw Self.Error(.imageSourceError) 45 | } 46 | guard 47 | let image = ImageSource.createImage( 48 | with: imageSource, 49 | at: 0, 50 | options: nil 51 | ) 52 | else { 53 | throw Self.Error(.imageError) 54 | } 55 | return image 56 | } 57 | } 58 | 59 | extension NetworkImageSerialization { 60 | struct Error : Swift.Error { 61 | enum Code { 62 | case imageSourceError 63 | case imageError 64 | } 65 | 66 | let code: Self.Code 67 | let underlying: Swift.Error? 68 | 69 | init( 70 | _ code: Self.Code, 71 | underlying: Swift.Error? = nil 72 | ) { 73 | self.code = code 74 | self.underlying = underlying 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Albums/NetworkJSONOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkJSONOperation.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol NetworkJSONOperationSession { 17 | static func data(for: URLRequest) async throws -> ( 18 | Data, 19 | URLResponse 20 | ) 21 | } 22 | 23 | extension NetworkSession : NetworkJSONOperationSession where URLSession == Foundation.URLSession { 24 | 25 | } 26 | 27 | protocol NetworkJSONOperationJSONHandler { 28 | associatedtype JSON 29 | 30 | static func json( 31 | with: Data, 32 | response: URLResponse 33 | ) throws -> JSON 34 | } 35 | 36 | extension NetworkJSONHandler : NetworkJSONOperationJSONHandler where DataHandler == NetworkDataHandler, JSONSerialization == Foundation.JSONSerialization { 37 | 38 | } 39 | 40 | struct NetworkJSONOperation< 41 | Session : NetworkJSONOperationSession, 42 | JSONHandler : NetworkJSONOperationJSONHandler 43 | > { 44 | 45 | } 46 | 47 | extension NetworkJSONOperation { 48 | static func json(for request: URLRequest) async throws -> JSONHandler.JSON { 49 | let ( 50 | data, 51 | response 52 | ) = try await { () -> ( 53 | Data, 54 | URLResponse 55 | ) in 56 | do { 57 | return try await Session.data(for: request) 58 | } catch { 59 | throw Self.Error( 60 | .sessionError, 61 | underlying: error 62 | ) 63 | } 64 | }() 65 | 66 | do { 67 | return try JSONHandler.json( 68 | with: data, 69 | response: response 70 | ) 71 | } catch { 72 | throw Self.Error( 73 | .jsonHandlerError, 74 | underlying: error 75 | ) 76 | } 77 | } 78 | } 79 | 80 | extension NetworkJSONOperation { 81 | struct Error : Swift.Error { 82 | enum Code { 83 | case sessionError 84 | case jsonHandlerError 85 | } 86 | 87 | let code: Self.Code 88 | let underlying: Swift.Error? 89 | 90 | init( 91 | _ code: Self.Code, 92 | underlying: Swift.Error? = nil 93 | ) { 94 | self.code = code 95 | self.underlying = underlying 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Albums/NetworkImageOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageOperation.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol NetworkImageOperationSession { 17 | static func data(for: URLRequest) async throws -> ( 18 | Data, 19 | URLResponse 20 | ) 21 | } 22 | 23 | extension NetworkSession : NetworkImageOperationSession where URLSession == Foundation.URLSession { 24 | 25 | } 26 | 27 | protocol NetworkImageOperationImageHandler { 28 | associatedtype Image 29 | 30 | static func image( 31 | with: Data, 32 | response: URLResponse 33 | ) throws -> Image 34 | } 35 | 36 | extension NetworkImageHandler : NetworkImageOperationImageHandler where DataHandler == NetworkDataHandler, ImageSerialization == NetworkImageSerialization { 37 | 38 | } 39 | 40 | struct NetworkImageOperation< 41 | Session : NetworkImageOperationSession, 42 | ImageHandler : NetworkImageOperationImageHandler 43 | > { 44 | 45 | } 46 | 47 | extension NetworkImageOperation { 48 | static func image(for request: URLRequest) async throws -> ImageHandler.Image { 49 | let ( 50 | data, 51 | response 52 | ) = try await { () -> ( 53 | Data, 54 | URLResponse 55 | ) in 56 | do { 57 | return try await Session.data(for: request) 58 | } catch { 59 | throw Self.Error( 60 | .sessionError, 61 | underlying: error 62 | ) 63 | } 64 | }() 65 | 66 | do { 67 | return try ImageHandler.image( 68 | with: data, 69 | response: response 70 | ) 71 | } catch { 72 | throw Self.Error( 73 | .imageHandlerError, 74 | underlying: error 75 | ) 76 | } 77 | } 78 | } 79 | 80 | extension NetworkImageOperation { 81 | struct Error : Swift.Error { 82 | enum Code { 83 | case sessionError 84 | case imageHandlerError 85 | } 86 | 87 | let code: Self.Code 88 | let underlying: Swift.Error? 89 | 90 | init( 91 | _ code: Self.Code, 92 | underlying: Swift.Error? = nil 93 | ) { 94 | self.code = code 95 | self.underlying = underlying 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Albums/AlbumsListModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsListModel.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol AlbumsListModelJSONOperation { 17 | associatedtype JSON 18 | 19 | static func json(for: URLRequest) async throws -> JSON 20 | } 21 | 22 | extension NetworkJSONOperation : AlbumsListModelJSONOperation where Session == NetworkSession, JSONHandler == NetworkJSONHandler { 23 | 24 | } 25 | 26 | struct Album { 27 | let id: String 28 | let artist: String 29 | let name: String 30 | let image: String 31 | } 32 | 33 | extension Album : Hashable { 34 | 35 | } 36 | 37 | extension Album : Identifiable { 38 | 39 | } 40 | 41 | @MainActor final class AlbumsListModel : ObservableObject { 42 | @Published private(set) var albums = Array() 43 | } 44 | 45 | private func Albums(_ json: Any) -> Array { 46 | var albums = Array() 47 | if let array = ((json as? Dictionary)?["feed"] as? Dictionary)?["entry"] as? Array> { 48 | for dictionary in array { 49 | if let artist = ((dictionary["im:artist"] as? Dictionary)?["label"] as? String), 50 | let name = ((dictionary["im:name"] as? Dictionary)?["label"] as? String), 51 | let image = ((dictionary["im:image"] as? Array>)?[2]["label"] as? String), 52 | let id = (((dictionary["id"] as? Dictionary)?["attributes"] as? Dictionary)?["im:id"] as? String) { 53 | let album = Album( 54 | id: id, 55 | artist: artist, 56 | name: name, 57 | image: image 58 | ) 59 | albums.append(album) 60 | } 61 | } 62 | } 63 | return albums 64 | } 65 | 66 | extension AlbumsListModel { 67 | func requestAlbums() async throws { 68 | if let url = URL(string: "https://itunes.apple.com/us/rss/topalbums/limit=100/json") { 69 | let request = URLRequest(url: url) 70 | let json = try await JSONOperation.json(for: request) 71 | self.albums = Albums(json) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Albums/NetworkImageHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageHandler.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol NetworkImageHandlerDataHandler { 17 | static func data( 18 | with: Data, 19 | response: URLResponse 20 | ) throws -> Data 21 | } 22 | 23 | extension NetworkDataHandler : NetworkImageHandlerDataHandler { 24 | 25 | } 26 | 27 | protocol NetworkImageHandlerImageSerialization { 28 | associatedtype Image 29 | 30 | static func image(with: Data) throws -> Image 31 | } 32 | 33 | extension NetworkImageSerialization : NetworkImageHandlerImageSerialization where ImageSource == NetworkImageSource { 34 | 35 | } 36 | 37 | struct NetworkImageHandler< 38 | DataHandler : NetworkImageHandlerDataHandler, 39 | ImageSerialization : NetworkImageHandlerImageSerialization 40 | > { 41 | 42 | } 43 | 44 | extension NetworkImageHandler { 45 | static func image( 46 | with data: Data, 47 | response: URLResponse 48 | ) throws -> ImageSerialization.Image { 49 | guard 50 | let mimeType = response.mimeType?.lowercased(), 51 | mimeType == "image/png" 52 | else { 53 | throw Self.Error(.mimeTypeError) 54 | } 55 | 56 | let data = try { () -> Data in 57 | do { 58 | return try DataHandler.data( 59 | with: data, 60 | response: response 61 | ) 62 | } catch { 63 | throw Self.Error( 64 | .dataHandlerError, 65 | underlying: error 66 | ) 67 | } 68 | }() 69 | 70 | do { 71 | return try ImageSerialization.image(with: data) 72 | } catch { 73 | throw Self.Error( 74 | .imageSerializationError, 75 | underlying: error 76 | ) 77 | } 78 | } 79 | } 80 | 81 | extension NetworkImageHandler { 82 | struct Error : Swift.Error { 83 | enum Code { 84 | case mimeTypeError 85 | case dataHandlerError 86 | case imageSerializationError 87 | } 88 | 89 | let code: Self.Code 90 | let underlying: Swift.Error? 91 | 92 | init( 93 | _ code: Self.Code, 94 | underlying: Swift.Error? = nil 95 | ) { 96 | self.code = code 97 | self.underlying = underlying 98 | } 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /Albums/NetworkJSONHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkJSONHandler.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import Foundation 15 | 16 | protocol NetworkJSONHandlerDataHandler { 17 | static func data( 18 | with: Data, 19 | response: URLResponse 20 | ) throws -> Data 21 | } 22 | 23 | extension NetworkDataHandler : NetworkJSONHandlerDataHandler { 24 | 25 | } 26 | 27 | protocol NetworkJSONHandlerJSONSerialization { 28 | associatedtype JSON 29 | 30 | static func jsonObject( 31 | with: Data, 32 | options: JSONSerialization.ReadingOptions 33 | ) throws -> JSON 34 | } 35 | 36 | extension JSONSerialization : NetworkJSONHandlerJSONSerialization { 37 | 38 | } 39 | 40 | struct NetworkJSONHandler< 41 | DataHandler : NetworkJSONHandlerDataHandler, 42 | JSONSerialization : NetworkJSONHandlerJSONSerialization 43 | > { 44 | 45 | } 46 | 47 | extension NetworkJSONHandler { 48 | static func json( 49 | with data: Data, 50 | response: URLResponse 51 | ) throws -> JSONSerialization.JSON { 52 | guard 53 | let mimeType = response.mimeType?.lowercased(), 54 | mimeType == "text/javascript" 55 | else { 56 | throw Self.Error(.mimeTypeError) 57 | } 58 | 59 | let data = try { () -> Data in 60 | do { 61 | return try DataHandler.data( 62 | with: data, 63 | response: response 64 | ) 65 | } catch { 66 | throw Self.Error( 67 | .dataHandlerError, 68 | underlying: error 69 | ) 70 | } 71 | }() 72 | 73 | do { 74 | return try JSONSerialization.jsonObject( 75 | with: data, 76 | options: [] 77 | ) 78 | } catch { 79 | throw Self.Error( 80 | .jsonSerializationError, 81 | underlying: error 82 | ) 83 | } 84 | } 85 | } 86 | 87 | extension NetworkJSONHandler { 88 | struct Error : Swift.Error { 89 | enum Code { 90 | case mimeTypeError 91 | case dataHandlerError 92 | case jsonSerializationError 93 | } 94 | 95 | let code: Self.Code 96 | let underlying: Swift.Error? 97 | 98 | init( 99 | _ code: Self.Code, 100 | underlying: Swift.Error? = nil 101 | ) { 102 | self.code = code 103 | self.underlying = underlying 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkDataHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkDataHandlerTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class NetworkDataHandlerTestCase : XCTestCase { 17 | 18 | } 19 | 20 | extension NetworkDataHandlerTestCase { 21 | func testError() { 22 | XCTAssertThrowsError( 23 | try NetworkDataHandler.data( 24 | with: DataTestDouble(), 25 | response: URLResponseTestDouble() 26 | ) 27 | ) { error in 28 | if let error = try? XCTUnwrap(error as? NetworkDataHandler.Error) { 29 | XCTAssertEqual( 30 | error.code, 31 | .statusCodeError 32 | ) 33 | XCTAssertNil(error.underlying) 34 | } 35 | } 36 | } 37 | } 38 | 39 | extension NetworkDataHandlerTestCase { 40 | private static var errorCodes: Array { 41 | return Array(100...199) + Array(300...599) 42 | } 43 | } 44 | 45 | extension NetworkDataHandlerTestCase { 46 | func testErrorWithStatusCode() { 47 | for statusCode in Self.errorCodes { 48 | XCTAssertThrowsError( 49 | try NetworkDataHandler.data( 50 | with: DataTestDouble(), 51 | response: HTTPURLResponseTestDouble(statusCode: statusCode) 52 | ), 53 | "Status Code \(statusCode)" 54 | ) { error in 55 | if let error = try? XCTUnwrap( 56 | error as? NetworkDataHandler.Error, 57 | "Status Code \(statusCode)" 58 | ) { 59 | XCTAssertEqual( 60 | error.code, 61 | .statusCodeError, 62 | "Status Code \(statusCode)" 63 | ) 64 | XCTAssertNil( 65 | error.underlying, 66 | "Status Code \(statusCode)" 67 | ) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | extension NetworkDataHandlerTestCase { 75 | private static var successCodes: Array { 76 | return Array(200...299) 77 | } 78 | } 79 | 80 | extension NetworkDataHandlerTestCase { 81 | func testSuccess() { 82 | for statusCode in Self.successCodes { 83 | XCTAssertNoThrow( 84 | try { 85 | let data = try NetworkDataHandler.data( 86 | with: DataTestDouble(), 87 | response: HTTPURLResponseTestDouble(statusCode: statusCode) 88 | ) 89 | XCTAssertEqual( 90 | data, 91 | DataTestDouble(), 92 | "Status Code \(statusCode)" 93 | ) 94 | }(), 95 | "Status Code \(statusCode)" 96 | ) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkSessionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkSessionTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class NetworkSessionTestCase : XCTestCase { 17 | private typealias NetworkSessionTestDouble = NetworkSession 18 | } 19 | 20 | extension NetworkSessionTestCase { 21 | final private class URLSessionTestDouble : NetworkSessionURLSession { 22 | static let shared = URLSessionTestDouble() 23 | 24 | var parameterRequest: URLRequest? 25 | var parameterDelegate: URLSessionTaskDelegate? 26 | var returnData: Data? 27 | var returnResponse: URLResponse? 28 | let returnError = NSErrorTestDouble() 29 | 30 | func data( 31 | for request: URLRequest, 32 | delegate: URLSessionTaskDelegate? 33 | ) async throws -> ( 34 | Data, 35 | URLResponse 36 | ) { 37 | self.parameterRequest = request 38 | self.parameterDelegate = delegate 39 | guard 40 | let returnData = self.returnData, 41 | let returnResponse = self.returnResponse 42 | else { 43 | throw self.returnError 44 | } 45 | return ( 46 | returnData, 47 | returnResponse 48 | ) 49 | } 50 | } 51 | } 52 | 53 | extension NetworkSessionTestCase { 54 | override func tearDown() { 55 | URLSessionTestDouble.shared.parameterRequest = nil 56 | URLSessionTestDouble.shared.parameterDelegate = nil 57 | URLSessionTestDouble.shared.returnData = nil 58 | URLSessionTestDouble.shared.returnResponse = nil 59 | } 60 | } 61 | 62 | extension NetworkSessionTestCase { 63 | func testError() async { 64 | URLSessionTestDouble.shared.returnData = nil 65 | URLSessionTestDouble.shared.returnResponse = nil 66 | 67 | do { 68 | _ = try await NetworkSessionTestDouble.data(for: URLRequestTestDouble()) 69 | XCTFail() 70 | } catch { 71 | XCTAssertEqual( 72 | URLSessionTestDouble.shared.parameterRequest, 73 | URLRequestTestDouble() 74 | ) 75 | XCTAssertNil(URLSessionTestDouble.shared.parameterDelegate) 76 | 77 | if let error = try? XCTUnwrap(error as NSError?) { 78 | XCTAssertIdentical( 79 | error, 80 | URLSessionTestDouble.shared.returnError 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | 87 | extension NetworkSessionTestCase { 88 | func testSuccess() async { 89 | URLSessionTestDouble.shared.returnData = DataTestDouble() 90 | URLSessionTestDouble.shared.returnResponse = URLResponseTestDouble() 91 | 92 | do { 93 | let ( 94 | data, 95 | response 96 | ) = try await NetworkSessionTestDouble.data(for: URLRequestTestDouble()) 97 | 98 | XCTAssertEqual( 99 | URLSessionTestDouble.shared.parameterRequest, 100 | URLRequestTestDouble() 101 | ) 102 | XCTAssertNil(URLSessionTestDouble.shared.parameterDelegate) 103 | 104 | XCTAssertEqual( 105 | data, 106 | URLSessionTestDouble.shared.returnData 107 | ) 108 | XCTAssertIdentical( 109 | response, 110 | URLSessionTestDouble.shared.returnResponse 111 | ) 112 | } catch { 113 | XCTFail() 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Albums/AlbumsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsListView.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import SwiftUI 15 | 16 | @MainActor protocol AlbumsListViewModel : ObservableObject { 17 | var albums: Array { get } 18 | 19 | func requestAlbums() async throws 20 | } 21 | 22 | extension AlbumsListModel : AlbumsListViewModel where JSONOperation == NetworkJSONOperation, NetworkJSONHandler> { 23 | 24 | } 25 | 26 | struct AlbumsListView : View { 27 | @ObservedObject private var model: ListViewModel 28 | 29 | init(model: ListViewModel) { 30 | self.model = model 31 | } 32 | } 33 | 34 | extension AlbumsListView { 35 | var body: some View { 36 | List( 37 | self.model.albums 38 | ) { album in 39 | AlbumsListRowView( 40 | model: ListRowViewModel(album: album) 41 | ) 42 | }.listStyle( 43 | .plain 44 | ).task { 45 | do { 46 | try await self.model.requestAlbums() 47 | } catch { 48 | print(error) 49 | } 50 | } 51 | } 52 | } 53 | 54 | struct AlbumsListView_Previews: PreviewProvider { 55 | 56 | } 57 | 58 | extension AlbumsListView_Previews { 59 | private final class ListModel : AlbumsListViewModel { 60 | @Published private(set) var albums = Array() 61 | 62 | func requestAlbums() async throws { 63 | self.albums = [ 64 | Album( 65 | id: "Rubber Soul", 66 | artist: "Beatles", 67 | name: "Rubber Soul", 68 | image: "http://localhost/rubber-soul.jpeg" 69 | ), 70 | Album( 71 | id: "Pet Sounds", 72 | artist: "Beach Boys", 73 | name: "Pet Sounds", 74 | image: "http://localhost/pet-sounds.jpeg" 75 | ), 76 | ] 77 | } 78 | } 79 | } 80 | 81 | extension AlbumsListView_Previews { 82 | private final class ListRowModel : AlbumsListRowViewModel { 83 | let artist: String 84 | let name: String 85 | 86 | @Published private(set) var image: CGImage? 87 | 88 | init(album: Album) { 89 | self.artist = album.artist 90 | self.name = album.name 91 | } 92 | 93 | func requestImage() async throws { 94 | if let context = CGContext( 95 | data: nil, 96 | width: 256, 97 | height: 256, 98 | bitsPerComponent: 8, 99 | bytesPerRow: 0, 100 | space: CGColorSpaceCreateDeviceRGB(), 101 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 102 | ) { 103 | context.setFillColor( 104 | red: 0.5, 105 | green: 0.5, 106 | blue: 0.5, 107 | alpha: 1 108 | ) 109 | context.fill( 110 | CGRect( 111 | x: 0, 112 | y: 0, 113 | width: 256, 114 | height: 256 115 | ) 116 | ) 117 | self.image = context.makeImage() 118 | } 119 | } 120 | } 121 | } 122 | 123 | extension AlbumsListView_Previews { 124 | static var previews: some View { 125 | AlbumsListView( 126 | model: ListModel() 127 | ) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /AlbumsTests/AlbumsListRowModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsListRowModelTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class AlbumsListRowModelTestCase : XCTestCase { 17 | private typealias AlbumsListRowModelTestDouble = AlbumsListRowModel 18 | } 19 | 20 | extension AlbumsListRowModelTestCase { 21 | private struct ImageOperationTestDouble : AlbumsListRowModelImageOperation { 22 | static var parameterRequest: URLRequest? 23 | static var returnImage: NSObject? 24 | static let returnError = NSErrorTestDouble() 25 | 26 | static func image(for request: URLRequest) async throws -> NSObject { 27 | self.parameterRequest = request 28 | guard 29 | let returnImage = self.returnImage 30 | else { 31 | throw self.returnError 32 | } 33 | return returnImage 34 | } 35 | } 36 | } 37 | 38 | extension AlbumsListRowModelTestCase { 39 | override func tearDown() { 40 | ImageOperationTestDouble.parameterRequest = nil 41 | ImageOperationTestDouble.returnImage = nil 42 | } 43 | } 44 | 45 | extension AlbumsListRowModelTestCase { 46 | private static var album: Album { 47 | return Album( 48 | id: "id", 49 | artist: "artist", 50 | name: "name", 51 | image: "image" 52 | ) 53 | } 54 | } 55 | 56 | extension AlbumsListRowModelTestCase { 57 | @MainActor func testError() async { 58 | ImageOperationTestDouble.returnImage = nil 59 | 60 | let model = AlbumsListRowModelTestDouble(album: Self.album) 61 | 62 | XCTAssertEqual( 63 | model.artist, 64 | Self.album.artist 65 | ) 66 | XCTAssertEqual( 67 | model.name, 68 | Self.album.name 69 | ) 70 | 71 | do { 72 | try await model.requestImage() 73 | XCTFail() 74 | } catch { 75 | XCTAssertEqual( 76 | ImageOperationTestDouble.parameterRequest, 77 | URLRequest(url: URL(string: Self.album.image)!) 78 | ) 79 | 80 | XCTAssertNil(model.image) 81 | 82 | if let error = try? XCTUnwrap(error as NSError?) { 83 | XCTAssertIdentical( 84 | error, 85 | ImageOperationTestDouble.returnError 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | 92 | extension AlbumsListRowModelTestCase { 93 | @MainActor func testSuccess() async { 94 | ImageOperationTestDouble.returnImage = NSObject() 95 | 96 | let model = AlbumsListRowModelTestDouble(album: Self.album) 97 | 98 | var modelDidChange = false 99 | let modelWillChange = model.objectWillChange.sink() { _ in 100 | modelDidChange = true 101 | } 102 | 103 | var imageDidChange = false 104 | let imageWillChange = model.$image.sink() { _ in 105 | if modelDidChange { 106 | imageDidChange = true 107 | } 108 | } 109 | 110 | XCTAssertEqual( 111 | model.artist, 112 | Self.album.artist 113 | ) 114 | XCTAssertEqual( 115 | model.name, 116 | Self.album.name 117 | ) 118 | 119 | do { 120 | try await model.requestImage() 121 | 122 | XCTAssertTrue(imageDidChange) 123 | 124 | XCTAssertEqual( 125 | ImageOperationTestDouble.parameterRequest, 126 | URLRequest(url: URL(string: Self.album.image)!) 127 | ) 128 | 129 | XCTAssertIdentical( 130 | model.image, 131 | ImageOperationTestDouble.returnImage 132 | ) 133 | } catch { 134 | XCTFail() 135 | } 136 | 137 | modelWillChange.cancel() 138 | imageWillChange.cancel() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Albums/AlbumsListRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsListRowView.swift 3 | // Albums 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import SwiftUI 15 | 16 | @MainActor protocol AlbumsListRowViewModel : ObservableObject { 17 | var artist: String { get } 18 | var name: String { get } 19 | var image: CGImage? { get } 20 | 21 | init(album: Album) 22 | 23 | func requestImage() async throws 24 | } 25 | 26 | extension AlbumsListRowModel : AlbumsListRowViewModel where ImageOperation == NetworkImageOperation, NetworkImageHandler>> { 27 | 28 | } 29 | 30 | struct AlbumsListRowView : View { 31 | @ObservedObject private var model: ListRowViewModel 32 | 33 | init(model: ListRowViewModel) { 34 | self.model = model 35 | } 36 | } 37 | 38 | extension AlbumsListRowView { 39 | var body: some View { 40 | HStack { 41 | if let cgImage = self.model.image { 42 | Image( 43 | decorative: cgImage, 44 | scale: 1.0, 45 | orientation: .up 46 | ).resizable( 47 | ).aspectRatio( 48 | contentMode: .fit 49 | ).frame( 50 | width: 128, 51 | height: 128, 52 | alignment: .topLeading 53 | ) 54 | } 55 | VStack( 56 | alignment: .leading, 57 | spacing: 3 58 | ) { 59 | Text( 60 | self.model.artist 61 | ).foregroundColor( 62 | .primary 63 | ).font( 64 | .headline 65 | ) 66 | Text( 67 | self.model.name 68 | ).foregroundColor( 69 | .secondary 70 | ).font( 71 | .subheadline 72 | ) 73 | } 74 | }.task { 75 | do { 76 | try await self.model.requestImage() 77 | } catch { 78 | print(error) 79 | } 80 | } 81 | } 82 | } 83 | 84 | struct AlbumsListRowView_Previews: PreviewProvider { 85 | 86 | } 87 | 88 | extension AlbumsListRowView_Previews { 89 | private final class ListRowModel : AlbumsListRowViewModel { 90 | let artist: String 91 | let name: String 92 | 93 | @Published private(set) var image: CGImage? 94 | 95 | init(album: Album) { 96 | self.artist = album.artist 97 | self.name = album.name 98 | } 99 | 100 | func requestImage() async throws { 101 | if let context = CGContext( 102 | data: nil, 103 | width: 256, 104 | height: 256, 105 | bitsPerComponent: 8, 106 | bytesPerRow: 0, 107 | space: CGColorSpaceCreateDeviceRGB(), 108 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 109 | ) { 110 | context.setFillColor( 111 | red: 0.5, 112 | green: 0.5, 113 | blue: 0.5, 114 | alpha: 1 115 | ) 116 | context.fill( 117 | CGRect( 118 | x: 0, 119 | y: 0, 120 | width: 256, 121 | height: 256 122 | ) 123 | ) 124 | self.image = context.makeImage() 125 | } 126 | } 127 | } 128 | } 129 | 130 | extension AlbumsListRowView_Previews { 131 | static var albums: Array { 132 | return [ 133 | Album( 134 | id: "Rubber Soul", 135 | artist: "Beatles", 136 | name: "Rubber Soul", 137 | image: "http://localhost/rubber-soul.jpeg" 138 | ), 139 | Album( 140 | id: "Pet Sounds", 141 | artist: "Beach Boys", 142 | name: "Pet Sounds", 143 | image: "http://localhost/pet-sounds.jpeg" 144 | ), 145 | ] 146 | } 147 | } 148 | 149 | extension AlbumsListRowView_Previews { 150 | static var previews: some View { 151 | List( 152 | self.albums 153 | ) { album in 154 | AlbumsListRowView( 155 | model: ListRowModel(album: album) 156 | ) 157 | }.listStyle( 158 | .plain 159 | ) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /AlbumsTests/AlbumsListModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsListModelTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class AlbumsListModelTestCase : XCTestCase { 17 | private typealias AlbumsListModelTestDouble = AlbumsListModel 18 | } 19 | 20 | extension AlbumsListModelTestCase { 21 | private struct JSONOperationTestDouble : AlbumsListModelJSONOperation { 22 | static var parameterRequest: URLRequest? 23 | static var returnJSON: Any? 24 | static let returnError = NSErrorTestDouble() 25 | 26 | static func json(for request: URLRequest) async throws -> Any { 27 | self.parameterRequest = request 28 | guard 29 | let returnJSON = self.returnJSON 30 | else { 31 | throw self.returnError 32 | } 33 | return returnJSON 34 | } 35 | } 36 | } 37 | 38 | extension AlbumsListModelTestCase { 39 | override func tearDown() { 40 | JSONOperationTestDouble.parameterRequest = nil 41 | JSONOperationTestDouble.returnJSON = nil 42 | } 43 | } 44 | 45 | extension AlbumsListModelTestCase { 46 | private static var request: URLRequest { 47 | return URLRequest(url: URL(string: "https://itunes.apple.com/us/rss/topalbums/limit=100/json")!) 48 | } 49 | } 50 | 51 | extension AlbumsListModelTestCase { 52 | private static var json: Any { 53 | let bundle = Bundle(identifier: "com.northbronson.AlbumsTests")! 54 | let url = bundle.url( 55 | forResource: "Albums", 56 | withExtension: "json" 57 | )! 58 | let data = try! Data(contentsOf: url) 59 | let json = try! JSONSerialization.jsonObject( 60 | with: data, 61 | options: [] 62 | ) 63 | return json 64 | } 65 | } 66 | 67 | private func Albums(_ json: Any) -> Array { 68 | var albums = Array() 69 | if let array = ((json as? Dictionary)?["feed"] as? Dictionary)?["entry"] as? Array> { 70 | for dictionary in array { 71 | if let artist = ((dictionary["im:artist"] as? Dictionary)?["label"] as? String), 72 | let name = ((dictionary["im:name"] as? Dictionary)?["label"] as? String), 73 | let image = ((dictionary["im:image"] as? Array>)?[2]["label"] as? String), 74 | let id = (((dictionary["id"] as? Dictionary)?["attributes"] as? Dictionary)?["im:id"] as? String) { 75 | let album = Album( 76 | id: id, 77 | artist: artist, 78 | name: name, 79 | image: image 80 | ) 81 | albums.append(album) 82 | } 83 | } 84 | } 85 | return albums 86 | } 87 | 88 | extension AlbumsListModelTestCase { 89 | private static var albums: Array { 90 | return Albums(self.json) 91 | } 92 | } 93 | 94 | extension AlbumsListModelTestCase { 95 | @MainActor func testError() async { 96 | JSONOperationTestDouble.returnJSON = nil 97 | 98 | let model = AlbumsListModelTestDouble() 99 | do { 100 | try await model.requestAlbums() 101 | XCTFail() 102 | } catch { 103 | XCTAssertEqual( 104 | JSONOperationTestDouble.parameterRequest, 105 | Self.request 106 | ) 107 | 108 | XCTAssertEqual( 109 | model.albums, 110 | [] 111 | ) 112 | 113 | if let error = try? XCTUnwrap(error as NSError?) { 114 | XCTAssertIdentical( 115 | error, 116 | JSONOperationTestDouble.returnError 117 | ) 118 | } 119 | } 120 | } 121 | } 122 | 123 | extension AlbumsListModelTestCase { 124 | @MainActor func testSuccess() async { 125 | JSONOperationTestDouble.returnJSON = Self.json 126 | 127 | let model = AlbumsListModelTestDouble() 128 | 129 | var modelDidChange = false 130 | let modelWillChange = model.objectWillChange.sink() { _ in 131 | modelDidChange = true 132 | } 133 | 134 | var albumsDidChange = false 135 | let albumsWillChange = model.$albums.sink() { _ in 136 | if modelDidChange { 137 | albumsDidChange = true 138 | } 139 | } 140 | 141 | do { 142 | try await model.requestAlbums() 143 | 144 | XCTAssertTrue(albumsDidChange) 145 | 146 | XCTAssertEqual( 147 | JSONOperationTestDouble.parameterRequest, 148 | Self.request 149 | ) 150 | 151 | XCTAssertEqual( 152 | model.albums, 153 | Self.albums 154 | ) 155 | } catch { 156 | XCTFail() 157 | } 158 | 159 | modelWillChange.cancel() 160 | albumsWillChange.cancel() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkImageSerializationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageSerializationTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class NetworkImageSerializationTestCase : XCTestCase { 17 | private typealias NetworkImageSerializationTestDouble = NetworkImageSerialization 18 | } 19 | 20 | extension NetworkImageSerializationTestCase { 21 | private struct ImageSourceTestDouble : NetworkImageSerializationImageSource { 22 | static var imageSourceParameterData: CFData? 23 | static var imageSourceParameterOptions: CFDictionary? 24 | static var imageSourceReturnImageSource: NSObject? 25 | 26 | static func createImageSource( 27 | with data: CFData, 28 | options: CFDictionary? 29 | ) -> NSObject? { 30 | self.imageSourceParameterData = data 31 | self.imageSourceParameterOptions = options 32 | return self.imageSourceReturnImageSource 33 | } 34 | 35 | static var imageParameterImageSource: NSObject? 36 | static var imageParameterIndex: Int? 37 | static var imageParameterOptions: CFDictionary? 38 | static var imageReturnImage: NSObject? 39 | 40 | static func createImage( 41 | with imageSource: NSObject, 42 | at index: Int, 43 | options: CFDictionary? 44 | ) -> NSObject? { 45 | self.imageParameterImageSource = imageSource 46 | self.imageParameterIndex = index 47 | self.imageParameterOptions = options 48 | return self.imageReturnImage 49 | } 50 | } 51 | } 52 | 53 | extension NetworkImageSerializationTestCase { 54 | override func tearDown() { 55 | ImageSourceTestDouble.imageSourceParameterData = nil 56 | ImageSourceTestDouble.imageSourceParameterOptions = nil 57 | ImageSourceTestDouble.imageSourceReturnImageSource = nil 58 | 59 | ImageSourceTestDouble.imageParameterImageSource = nil 60 | ImageSourceTestDouble.imageParameterIndex = nil 61 | ImageSourceTestDouble.imageParameterOptions = nil 62 | ImageSourceTestDouble.imageReturnImage = nil 63 | } 64 | } 65 | 66 | extension NetworkImageSerializationTestCase { 67 | func testImageSourceError() { 68 | ImageSourceTestDouble.imageSourceReturnImageSource = nil 69 | 70 | ImageSourceTestDouble.imageReturnImage = nil 71 | 72 | XCTAssertThrowsError( 73 | try NetworkImageSerializationTestDouble.image(with: DataTestDouble()) 74 | ) { error in 75 | XCTAssertEqual( 76 | ImageSourceTestDouble.imageSourceParameterData as Data?, 77 | DataTestDouble() 78 | ) 79 | XCTAssertNil(ImageSourceTestDouble.imageSourceParameterOptions) 80 | 81 | XCTAssertNil(ImageSourceTestDouble.imageParameterImageSource) 82 | XCTAssertNil(ImageSourceTestDouble.imageParameterIndex) 83 | XCTAssertNil(ImageSourceTestDouble.imageParameterOptions) 84 | 85 | if let error = try? XCTUnwrap(error as? NetworkImageSerializationTestDouble.Error) { 86 | XCTAssertEqual( 87 | error.code, 88 | .imageSourceError 89 | ) 90 | XCTAssertNil(error.underlying) 91 | } 92 | } 93 | } 94 | } 95 | 96 | extension NetworkImageSerializationTestCase { 97 | func testImageError() { 98 | ImageSourceTestDouble.imageSourceReturnImageSource = NSObject() 99 | 100 | ImageSourceTestDouble.imageReturnImage = nil 101 | 102 | XCTAssertThrowsError( 103 | try NetworkImageSerializationTestDouble.image(with: DataTestDouble()) 104 | ) { error in 105 | XCTAssertEqual( 106 | ImageSourceTestDouble.imageSourceParameterData as Data?, 107 | DataTestDouble() 108 | ) 109 | XCTAssertNil(ImageSourceTestDouble.imageSourceParameterOptions) 110 | 111 | XCTAssertIdentical( 112 | ImageSourceTestDouble.imageParameterImageSource, 113 | ImageSourceTestDouble.imageSourceReturnImageSource 114 | ) 115 | XCTAssertEqual( 116 | ImageSourceTestDouble.imageParameterIndex, 117 | 0 118 | ) 119 | XCTAssertNil(ImageSourceTestDouble.imageSourceParameterOptions) 120 | 121 | if let error = try? XCTUnwrap(error as? NetworkImageSerializationTestDouble.Error) { 122 | XCTAssertEqual( 123 | error.code, 124 | .imageError 125 | ) 126 | XCTAssertNil(error.underlying) 127 | } 128 | } 129 | } 130 | } 131 | 132 | extension NetworkImageSerializationTestCase { 133 | func testSuccess() { 134 | ImageSourceTestDouble.imageSourceReturnImageSource = NSObject() 135 | 136 | ImageSourceTestDouble.imageReturnImage = NSObject() 137 | 138 | XCTAssertNoThrow( 139 | try { 140 | let image = try NetworkImageSerializationTestDouble.image(with: DataTestDouble()) 141 | 142 | XCTAssertEqual( 143 | ImageSourceTestDouble.imageSourceParameterData as Data?, 144 | DataTestDouble() 145 | ) 146 | XCTAssertNil(ImageSourceTestDouble.imageSourceParameterOptions) 147 | 148 | XCTAssertIdentical( 149 | ImageSourceTestDouble.imageParameterImageSource, 150 | ImageSourceTestDouble.imageSourceReturnImageSource 151 | ) 152 | XCTAssertEqual( 153 | ImageSourceTestDouble.imageParameterIndex, 154 | 0 155 | ) 156 | XCTAssertNil(ImageSourceTestDouble.imageSourceParameterOptions) 157 | 158 | XCTAssertIdentical( 159 | image, 160 | ImageSourceTestDouble.imageReturnImage 161 | ) 162 | }() 163 | ) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkJSONOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkJSONOperationTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class NetworkJSONOperationTestCase : XCTestCase { 17 | private typealias NetworkJSONOperationTestDouble = NetworkJSONOperation 18 | } 19 | 20 | extension NetworkJSONOperationTestCase { 21 | private struct SessionTestDouble : NetworkJSONOperationSession { 22 | static var parameterRequest: URLRequest? 23 | static var returnData: Data? 24 | static var returnResponse: URLResponse? 25 | static let returnError = NSErrorTestDouble() 26 | 27 | static func data(for request: URLRequest) async throws -> ( 28 | Data, 29 | URLResponse 30 | ) { 31 | self.parameterRequest = request 32 | guard 33 | let returnData = self.returnData, 34 | let returnResponse = self.returnResponse 35 | else { 36 | throw self.returnError 37 | } 38 | return ( 39 | returnData, 40 | returnResponse 41 | ) 42 | } 43 | } 44 | } 45 | 46 | extension NetworkJSONOperationTestCase { 47 | private struct JSONHandlerTestDouble : NetworkJSONOperationJSONHandler { 48 | static var parameterData: Data? 49 | static var parameterResponse: URLResponse? 50 | static var returnJSON: NSObject? 51 | static let returnError = NSErrorTestDouble() 52 | 53 | static func json( 54 | with data: Data, 55 | response: URLResponse 56 | ) throws -> NSObject { 57 | self.parameterData = data 58 | self.parameterResponse = response 59 | guard 60 | let returnJSON = self.returnJSON 61 | else { 62 | throw self.returnError 63 | } 64 | return returnJSON 65 | } 66 | } 67 | } 68 | 69 | extension NetworkJSONOperationTestCase { 70 | override func tearDown() { 71 | SessionTestDouble.parameterRequest = nil 72 | SessionTestDouble.returnData = nil 73 | SessionTestDouble.returnResponse = nil 74 | 75 | JSONHandlerTestDouble.parameterData = nil 76 | JSONHandlerTestDouble.parameterResponse = nil 77 | JSONHandlerTestDouble.returnJSON = nil 78 | } 79 | } 80 | 81 | extension NetworkJSONOperationTestCase { 82 | func testSessionError() async { 83 | SessionTestDouble.returnData = nil 84 | SessionTestDouble.returnResponse = nil 85 | 86 | JSONHandlerTestDouble.returnJSON = nil 87 | 88 | do { 89 | _ = try await NetworkJSONOperationTestDouble.json(for: URLRequestTestDouble()) 90 | XCTFail() 91 | } catch { 92 | XCTAssertEqual( 93 | SessionTestDouble.parameterRequest, 94 | URLRequestTestDouble() 95 | ) 96 | 97 | XCTAssertNil(JSONHandlerTestDouble.parameterData) 98 | XCTAssertNil(JSONHandlerTestDouble.parameterResponse) 99 | 100 | if let error = try? XCTUnwrap(error as? NetworkJSONOperationTestDouble.Error) { 101 | XCTAssertEqual( 102 | error.code, 103 | .sessionError 104 | ) 105 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 106 | XCTAssertIdentical( 107 | underlying, 108 | SessionTestDouble.returnError 109 | ) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | extension NetworkJSONOperationTestCase { 117 | func testJSONHandlerError() async { 118 | SessionTestDouble.returnData = DataTestDouble() 119 | SessionTestDouble.returnResponse = HTTPURLResponseTestDouble() 120 | 121 | JSONHandlerTestDouble.returnJSON = nil 122 | 123 | do { 124 | _ = try await NetworkJSONOperationTestDouble.json(for: URLRequestTestDouble()) 125 | XCTFail() 126 | } catch { 127 | XCTAssertEqual( 128 | SessionTestDouble.parameterRequest, 129 | URLRequestTestDouble() 130 | ) 131 | 132 | XCTAssertEqual( 133 | JSONHandlerTestDouble.parameterData, 134 | SessionTestDouble.returnData 135 | ) 136 | XCTAssertIdentical( 137 | JSONHandlerTestDouble.parameterResponse, 138 | SessionTestDouble.returnResponse 139 | ) 140 | 141 | if let error = try? XCTUnwrap(error as? NetworkJSONOperationTestDouble.Error) { 142 | XCTAssertEqual( 143 | error.code, 144 | .jsonHandlerError 145 | ) 146 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 147 | XCTAssertIdentical( 148 | underlying, 149 | JSONHandlerTestDouble.returnError 150 | ) 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | extension NetworkJSONOperationTestCase { 158 | func testSuccess() async { 159 | SessionTestDouble.returnData = DataTestDouble() 160 | SessionTestDouble.returnResponse = HTTPURLResponseTestDouble() 161 | 162 | JSONHandlerTestDouble.returnJSON = NSObject() 163 | 164 | do { 165 | let json = try await NetworkJSONOperationTestDouble.json(for: URLRequestTestDouble()) 166 | 167 | XCTAssertEqual( 168 | SessionTestDouble.parameterRequest, 169 | URLRequestTestDouble() 170 | ) 171 | 172 | XCTAssertEqual( 173 | JSONHandlerTestDouble.parameterData, 174 | SessionTestDouble.returnData 175 | ) 176 | XCTAssertIdentical( 177 | JSONHandlerTestDouble.parameterResponse, 178 | SessionTestDouble.returnResponse 179 | ) 180 | 181 | XCTAssertIdentical( 182 | json, 183 | JSONHandlerTestDouble.returnJSON 184 | ) 185 | } catch { 186 | XCTFail() 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkImageOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageOperationTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class NetworkImageOperationTestCase: XCTestCase { 17 | private typealias NetworkImageOperationTestDouble = NetworkImageOperation 18 | } 19 | 20 | extension NetworkImageOperationTestCase { 21 | private struct SessionTestDouble : NetworkImageOperationSession { 22 | static var parameterRequest: URLRequest? 23 | static var returnData: Data? 24 | static var returnResponse: URLResponse? 25 | static let returnError = NSErrorTestDouble() 26 | 27 | static func data(for request: URLRequest) async throws -> ( 28 | Data, 29 | URLResponse 30 | ) { 31 | self.parameterRequest = request 32 | guard 33 | let returnData = self.returnData, 34 | let returnResponse = self.returnResponse 35 | else { 36 | throw self.returnError 37 | } 38 | return ( 39 | returnData, 40 | returnResponse 41 | ) 42 | } 43 | } 44 | } 45 | 46 | extension NetworkImageOperationTestCase { 47 | private struct ImageHandlerTestDouble : NetworkImageOperationImageHandler { 48 | static var parameterData: Data? 49 | static var parameterResponse: URLResponse? 50 | static var returnImage: NSObject? 51 | static let returnError = NSErrorTestDouble() 52 | 53 | static func image( 54 | with data: Data, 55 | response: URLResponse 56 | ) throws -> NSObject { 57 | self.parameterData = data 58 | self.parameterResponse = response 59 | guard 60 | let returnImage = self.returnImage 61 | else { 62 | throw self.returnError 63 | } 64 | return returnImage 65 | } 66 | } 67 | } 68 | 69 | extension NetworkImageOperationTestCase { 70 | override func tearDown() { 71 | SessionTestDouble.parameterRequest = nil 72 | SessionTestDouble.returnData = nil 73 | SessionTestDouble.returnResponse = nil 74 | 75 | ImageHandlerTestDouble.parameterData = nil 76 | ImageHandlerTestDouble.parameterResponse = nil 77 | ImageHandlerTestDouble.returnImage = nil 78 | } 79 | } 80 | 81 | extension NetworkImageOperationTestCase { 82 | func testSessionError() async { 83 | SessionTestDouble.returnData = nil 84 | SessionTestDouble.returnResponse = nil 85 | 86 | ImageHandlerTestDouble.returnImage = nil 87 | 88 | do { 89 | _ = try await NetworkImageOperationTestDouble.image(for: URLRequestTestDouble()) 90 | XCTFail() 91 | } catch { 92 | XCTAssertEqual( 93 | SessionTestDouble.parameterRequest, 94 | URLRequestTestDouble() 95 | ) 96 | 97 | XCTAssertNil(ImageHandlerTestDouble.parameterData) 98 | XCTAssertNil(ImageHandlerTestDouble.parameterResponse) 99 | 100 | if let error = try? XCTUnwrap(error as? NetworkImageOperationTestDouble.Error) { 101 | XCTAssertEqual( 102 | error.code, 103 | .sessionError 104 | ) 105 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 106 | XCTAssertIdentical( 107 | underlying, 108 | SessionTestDouble.returnError 109 | ) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | extension NetworkImageOperationTestCase { 117 | func testImageHandlerError() async { 118 | SessionTestDouble.returnData = DataTestDouble() 119 | SessionTestDouble.returnResponse = HTTPURLResponseTestDouble() 120 | 121 | ImageHandlerTestDouble.returnImage = nil 122 | 123 | do { 124 | _ = try await NetworkImageOperationTestDouble.image(for: URLRequestTestDouble()) 125 | XCTFail() 126 | } catch { 127 | XCTAssertEqual( 128 | SessionTestDouble.parameterRequest, 129 | URLRequestTestDouble() 130 | ) 131 | 132 | XCTAssertEqual( 133 | ImageHandlerTestDouble.parameterData, 134 | SessionTestDouble.returnData 135 | ) 136 | XCTAssertIdentical( 137 | ImageHandlerTestDouble.parameterResponse, 138 | SessionTestDouble.returnResponse 139 | ) 140 | 141 | if let error = try? XCTUnwrap(error as? NetworkImageOperationTestDouble.Error) { 142 | XCTAssertEqual( 143 | error.code, 144 | .imageHandlerError 145 | ) 146 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 147 | XCTAssertIdentical( 148 | underlying, 149 | ImageHandlerTestDouble.returnError 150 | ) 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | extension NetworkImageOperationTestCase { 158 | func testSuccess() async { 159 | SessionTestDouble.returnData = DataTestDouble() 160 | SessionTestDouble.returnResponse = HTTPURLResponseTestDouble() 161 | 162 | ImageHandlerTestDouble.returnImage = NSObject() 163 | 164 | do { 165 | let image = try await NetworkImageOperationTestDouble.image(for: URLRequestTestDouble()) 166 | 167 | XCTAssertEqual( 168 | SessionTestDouble.parameterRequest, 169 | URLRequestTestDouble() 170 | ) 171 | 172 | XCTAssertEqual( 173 | ImageHandlerTestDouble.parameterData, 174 | SessionTestDouble.returnData 175 | ) 176 | XCTAssertIdentical( 177 | ImageHandlerTestDouble.parameterResponse, 178 | SessionTestDouble.returnResponse 179 | ) 180 | 181 | XCTAssertIdentical( 182 | image, 183 | ImageHandlerTestDouble.returnImage 184 | ) 185 | } catch { 186 | XCTFail() 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkImageSourceTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageSourceTests.m 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | #import 15 | 16 | #import "NetworkImageSource.h" 17 | 18 | static CGImageSourceRef _Nullable NetworkImageSourceTestDoubleCreateImageSource(CFDataRef _Nonnull, CFDictionaryRef _Nullable) CF_RETURNS_RETAINED; 19 | 20 | static id NetworkImageSourceTestDoubleCreateImageSourceParameterData = 0; 21 | static id NetworkImageSourceTestDoubleCreateImageSourceParameterOptions = 0; 22 | static id NetworkImageSourceTestDoubleCreateImageSourceReturnImageSource = 0; 23 | 24 | CGImageSourceRef NetworkImageSourceTestDoubleCreateImageSource(CFDataRef data, CFDictionaryRef options) { 25 | NetworkImageSourceTestDoubleCreateImageSourceParameterData = (__bridge id)data; 26 | NetworkImageSourceTestDoubleCreateImageSourceParameterOptions = (__bridge id)options; 27 | return (__bridge_retained CGImageSourceRef)NetworkImageSourceTestDoubleCreateImageSourceReturnImageSource; 28 | } 29 | 30 | static CGImageRef _Nullable NetworkImageSourceTestDoubleCreateImage(CGImageSourceRef _Nonnull, size_t, CFDictionaryRef _Nullable) CF_RETURNS_RETAINED; 31 | 32 | static id NetworkImageSourceTestDoubleCreateImageParameterImageSource = 0; 33 | static size_t NetworkImageSourceTestDoubleCreateImageParameterIndex = 0; 34 | static id NetworkImageSourceTestDoubleCreateImageParameterOptions = 0; 35 | static id NetworkImageSourceTestDoubleCreateImageReturnImage = 0; 36 | 37 | CGImageRef NetworkImageSourceTestDoubleCreateImage(CGImageSourceRef imageSource, size_t index, CFDictionaryRef options) { 38 | NetworkImageSourceTestDoubleCreateImageParameterImageSource = (__bridge id)imageSource; 39 | NetworkImageSourceTestDoubleCreateImageParameterIndex = index; 40 | NetworkImageSourceTestDoubleCreateImageParameterOptions = (__bridge id)options; 41 | return (__bridge_retained CGImageRef)NetworkImageSourceTestDoubleCreateImageReturnImage; 42 | } 43 | 44 | @interface NetworkImageSourceTestDouble : NetworkImageSource 45 | 46 | @end 47 | 48 | @implementation NetworkImageSourceTestDouble 49 | 50 | @end 51 | 52 | @implementation NetworkImageSourceTestDouble (CreateImageSource) 53 | 54 | + (CGImageSourceRef _Nullable (*_Nonnull)(CFDataRef _Nonnull, CFDictionaryRef _Nullable))createImageSource { 55 | return NetworkImageSourceTestDoubleCreateImageSource; 56 | } 57 | 58 | @end 59 | 60 | @implementation NetworkImageSourceTestDouble (CreateImage) 61 | 62 | + (CGImageRef _Nullable (*_Nonnull)(CGImageSourceRef _Nonnull, size_t, CFDictionaryRef _Nullable))createImage { 63 | return NetworkImageSourceTestDoubleCreateImage; 64 | } 65 | 66 | @end 67 | 68 | @interface NetworkImageSourceTestCase : XCTestCase 69 | 70 | @end 71 | 72 | @implementation NetworkImageSourceTestCase 73 | 74 | @end 75 | 76 | @implementation NetworkImageSourceTestCase (TearDown) 77 | 78 | - (void)tearDown { 79 | NetworkImageSourceTestDoubleCreateImageSourceParameterData = 0; 80 | NetworkImageSourceTestDoubleCreateImageSourceParameterOptions = 0; 81 | NetworkImageSourceTestDoubleCreateImageSourceReturnImageSource = 0; 82 | 83 | NetworkImageSourceTestDoubleCreateImageParameterImageSource = 0; 84 | NetworkImageSourceTestDoubleCreateImageParameterIndex = 0; 85 | NetworkImageSourceTestDoubleCreateImageParameterOptions = 0; 86 | NetworkImageSourceTestDoubleCreateImageReturnImage = 0; 87 | } 88 | 89 | @end 90 | 91 | @implementation NetworkImageSourceTestCase (CreateImageSource) 92 | 93 | - (void)testCreateImageSource { 94 | XCTAssert([NetworkImageSource createImageSource] == CGImageSourceCreateWithData); 95 | } 96 | 97 | @end 98 | 99 | @implementation NetworkImageSourceTestCase (CreateImage) 100 | 101 | - (void)testCreateImage { 102 | XCTAssert([NetworkImageSource createImage] == CGImageSourceCreateImageAtIndex); 103 | } 104 | 105 | @end 106 | 107 | @implementation NetworkImageSourceTestCase (CreateImageSourceTestDouble) 108 | 109 | - (void)testCreateImageSourceTestDouble { 110 | NetworkImageSourceTestDoubleCreateImageSourceReturnImageSource = [[NSObject alloc] init]; 111 | 112 | id data = [[NSObject alloc] init]; 113 | id options = [[NSObject alloc] init]; 114 | 115 | id imageSource = (__bridge_transfer id)[NetworkImageSourceTestDouble createImageSourceWithData:(__bridge CFDataRef)data 116 | options:(__bridge CFDictionaryRef)options]; 117 | 118 | XCTAssert(NetworkImageSourceTestDoubleCreateImageSourceParameterData == data); 119 | XCTAssert(NetworkImageSourceTestDoubleCreateImageSourceParameterOptions == options); 120 | 121 | XCTAssert(imageSource == NetworkImageSourceTestDoubleCreateImageSourceReturnImageSource); 122 | } 123 | 124 | @end 125 | 126 | @implementation NetworkImageSourceTestCase (CreateImageTestDouble) 127 | 128 | - (void)testCreateImageTestDouble { 129 | NetworkImageSourceTestDoubleCreateImageReturnImage = [[NSObject alloc] init]; 130 | 131 | id imageSource = [[NSObject alloc] init]; 132 | size_t index = 1; 133 | id options = [[NSObject alloc] init]; 134 | 135 | id image = (__bridge_transfer id)[NetworkImageSourceTestDouble createImageWithImageSource:(__bridge CGImageSourceRef)imageSource 136 | atIndex:index 137 | options:(__bridge CFDictionaryRef)options]; 138 | 139 | XCTAssert(NetworkImageSourceTestDoubleCreateImageParameterImageSource == imageSource); 140 | XCTAssert(NetworkImageSourceTestDoubleCreateImageParameterIndex == index); 141 | XCTAssert(NetworkImageSourceTestDoubleCreateImageParameterOptions == options); 142 | 143 | XCTAssert(image == NetworkImageSourceTestDoubleCreateImageReturnImage); 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkImageHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageHandlerTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class NetworkImageHandlerTestCase : XCTestCase { 17 | private typealias NetworkImageHandlerTestDouble = NetworkImageHandler 18 | } 19 | 20 | extension NetworkImageHandlerTestCase { 21 | private struct DataHandlerTestDouble : NetworkImageHandlerDataHandler { 22 | static var parameterData: Data? 23 | static var parameterResponse: URLResponse? 24 | static var returnData: Data? 25 | static let returnError = NSErrorTestDouble() 26 | 27 | static func data( 28 | with data: Data, 29 | response: URLResponse 30 | ) throws -> Data { 31 | self.parameterData = data 32 | self.parameterResponse = response 33 | guard 34 | let returnData = self.returnData 35 | else { 36 | throw self.returnError 37 | } 38 | return returnData 39 | } 40 | } 41 | } 42 | 43 | extension NetworkImageHandlerTestCase { 44 | private struct ImageSerializationTestDouble : NetworkImageHandlerImageSerialization { 45 | static var parameterData: Data? 46 | static var returnImage: NSObject? 47 | static let returnError = NSErrorTestDouble() 48 | 49 | static func image(with data: Data) throws -> NSObject { 50 | self.parameterData = data 51 | guard 52 | let returnImage = self.returnImage 53 | else { 54 | throw self.returnError 55 | } 56 | return returnImage 57 | } 58 | } 59 | } 60 | 61 | extension NetworkImageHandlerTestCase { 62 | override func tearDown() { 63 | DataHandlerTestDouble.parameterData = nil 64 | DataHandlerTestDouble.parameterResponse = nil 65 | DataHandlerTestDouble.returnData = nil 66 | 67 | ImageSerializationTestDouble.parameterData = nil 68 | ImageSerializationTestDouble.returnImage = nil 69 | } 70 | } 71 | 72 | extension NetworkImageHandlerTestCase { 73 | func testMimeTypeError() { 74 | DataHandlerTestDouble.returnData = nil 75 | 76 | ImageSerializationTestDouble.returnImage = nil 77 | 78 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "TEXT/JAVASCRIPT"]) 79 | 80 | XCTAssertThrowsError( 81 | try NetworkImageHandlerTestDouble.image( 82 | with: DataTestDouble(), 83 | response: response 84 | ) 85 | ) { error in 86 | XCTAssertNil(DataHandlerTestDouble.parameterData) 87 | XCTAssertNil(DataHandlerTestDouble.parameterResponse) 88 | 89 | XCTAssertNil(ImageSerializationTestDouble.parameterData) 90 | 91 | if let error = try? XCTUnwrap(error as? NetworkImageHandlerTestDouble.Error) { 92 | XCTAssertEqual( 93 | error.code, 94 | .mimeTypeError 95 | ) 96 | XCTAssertNil(error.underlying) 97 | } 98 | } 99 | } 100 | } 101 | 102 | extension NetworkImageHandlerTestCase { 103 | func testDataHandlerError() { 104 | DataHandlerTestDouble.returnData = nil 105 | 106 | ImageSerializationTestDouble.returnImage = nil 107 | 108 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "IMAGE/PNG"]) 109 | 110 | XCTAssertThrowsError( 111 | try NetworkImageHandlerTestDouble.image( 112 | with: DataTestDouble(), 113 | response: response 114 | ) 115 | ) { error in 116 | XCTAssertEqual( 117 | DataHandlerTestDouble.parameterData, 118 | DataTestDouble() 119 | ) 120 | XCTAssertIdentical( 121 | DataHandlerTestDouble.parameterResponse, 122 | response 123 | ) 124 | 125 | XCTAssertNil(ImageSerializationTestDouble.parameterData) 126 | 127 | if let error = try? XCTUnwrap(error as? NetworkImageHandlerTestDouble.Error) { 128 | XCTAssertEqual( 129 | error.code, 130 | .dataHandlerError 131 | ) 132 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 133 | XCTAssertIdentical( 134 | underlying, 135 | DataHandlerTestDouble.returnError 136 | ) 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | extension NetworkImageHandlerTestCase { 144 | func testImageSerializationError() { 145 | DataHandlerTestDouble.returnData = DataTestDouble() 146 | 147 | ImageSerializationTestDouble.returnImage = nil 148 | 149 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "IMAGE/PNG"]) 150 | 151 | XCTAssertThrowsError( 152 | try NetworkImageHandlerTestDouble.image( 153 | with: DataTestDouble(), 154 | response: response 155 | ) 156 | ) { error in 157 | XCTAssertEqual( 158 | DataHandlerTestDouble.parameterData, 159 | DataTestDouble() 160 | ) 161 | XCTAssertIdentical( 162 | DataHandlerTestDouble.parameterResponse, 163 | response 164 | ) 165 | 166 | XCTAssertEqual( 167 | ImageSerializationTestDouble.parameterData, 168 | DataHandlerTestDouble.returnData 169 | ) 170 | 171 | if let error = try? XCTUnwrap(error as? NetworkImageHandlerTestDouble.Error) { 172 | XCTAssertEqual( 173 | error.code, 174 | .imageSerializationError 175 | ) 176 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 177 | XCTAssertIdentical( 178 | underlying, 179 | ImageSerializationTestDouble.returnError 180 | ) 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | extension NetworkImageHandlerTestCase { 188 | func testSuccess() { 189 | DataHandlerTestDouble.returnData = DataTestDouble() 190 | 191 | ImageSerializationTestDouble.returnImage = NSObject() 192 | 193 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "IMAGE/PNG"]) 194 | 195 | XCTAssertNoThrow( 196 | try { 197 | let image = try NetworkImageHandlerTestDouble.image( 198 | with: DataTestDouble(), 199 | response: response 200 | ) 201 | 202 | XCTAssertEqual( 203 | DataHandlerTestDouble.parameterData, 204 | DataTestDouble() 205 | ) 206 | XCTAssertIdentical( 207 | DataHandlerTestDouble.parameterResponse, 208 | response 209 | ) 210 | 211 | XCTAssertEqual( 212 | ImageSerializationTestDouble.parameterData, 213 | DataHandlerTestDouble.returnData 214 | ) 215 | 216 | XCTAssertIdentical( 217 | image, 218 | ImageSerializationTestDouble.returnImage 219 | ) 220 | }() 221 | ) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /AlbumsTests/NetworkJSONHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkJSONHandlerTests.swift 3 | // AlbumsTests 4 | // 5 | // Copyright © 2021 North Bronson Software 6 | // 7 | // This Item is protected by copyright and/or related rights. You are free to use this Item in any way that is permitted by the copyright and related rights legislation that applies to your use. In addition, no permission is required from the rights-holder(s) for scholarly, educational, or non-commercial uses. For other uses, you need to obtain permission from the rights-holder(s). 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | // 13 | 14 | import XCTest 15 | 16 | final class NetworkJSONHandlerTestCase : XCTestCase { 17 | 18 | } 19 | 20 | extension NetworkJSONHandlerTestCase { 21 | private struct DataHandlerTestDouble : NetworkJSONHandlerDataHandler { 22 | static var parameterData: Data? 23 | static var parameterResponse: URLResponse? 24 | static var returnData: Data? 25 | static let returnError = NSErrorTestDouble() 26 | 27 | static func data( 28 | with data: Data, 29 | response: URLResponse 30 | ) throws -> Data { 31 | self.parameterData = data 32 | self.parameterResponse = response 33 | guard 34 | let returnData = self.returnData 35 | else { 36 | throw self.returnError 37 | } 38 | return returnData 39 | } 40 | } 41 | } 42 | 43 | extension NetworkJSONHandlerTestCase { 44 | private struct JSONSerializationTestDouble : NetworkJSONHandlerJSONSerialization { 45 | static var parameterData: Data? 46 | static var parameterOptions: JSONSerialization.ReadingOptions? 47 | static var returnJSON: NSObject? 48 | static let returnError = NSErrorTestDouble() 49 | 50 | static func jsonObject( 51 | with data: Data, 52 | options: JSONSerialization.ReadingOptions 53 | ) throws -> NSObject { 54 | self.parameterData = data 55 | self.parameterOptions = options 56 | guard 57 | let returnJSON = self.returnJSON 58 | else { 59 | throw self.returnError 60 | } 61 | return returnJSON 62 | } 63 | } 64 | } 65 | 66 | extension NetworkJSONHandlerTestCase { 67 | private typealias NetworkJSONHandlerTestDouble = NetworkJSONHandler 68 | } 69 | 70 | extension NetworkJSONHandlerTestCase { 71 | override func tearDown() { 72 | DataHandlerTestDouble.parameterData = nil 73 | DataHandlerTestDouble.parameterResponse = nil 74 | DataHandlerTestDouble.returnData = nil 75 | 76 | JSONSerializationTestDouble.parameterData = nil 77 | JSONSerializationTestDouble.parameterOptions = nil 78 | JSONSerializationTestDouble.returnJSON = nil 79 | } 80 | } 81 | 82 | extension NetworkJSONHandlerTestCase { 83 | func testMimeTypeError() { 84 | DataHandlerTestDouble.returnData = nil 85 | 86 | JSONSerializationTestDouble.returnJSON = nil 87 | 88 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "IMAGE/PNG"]) 89 | 90 | XCTAssertThrowsError( 91 | try NetworkJSONHandlerTestDouble.json( 92 | with: DataTestDouble(), 93 | response: response 94 | ) 95 | ) { error in 96 | XCTAssertNil(DataHandlerTestDouble.parameterData) 97 | XCTAssertNil(DataHandlerTestDouble.parameterResponse) 98 | 99 | XCTAssertNil(JSONSerializationTestDouble.parameterData) 100 | XCTAssertNil(JSONSerializationTestDouble.parameterOptions) 101 | 102 | if let error = try? XCTUnwrap(error as? NetworkJSONHandlerTestDouble.Error) { 103 | XCTAssertEqual( 104 | error.code, 105 | .mimeTypeError 106 | ) 107 | XCTAssertNil(error.underlying) 108 | } 109 | } 110 | } 111 | } 112 | 113 | extension NetworkJSONHandlerTestCase { 114 | func testDataHandlerError() { 115 | DataHandlerTestDouble.returnData = nil 116 | 117 | JSONSerializationTestDouble.returnJSON = nil 118 | 119 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "TEXT/JAVASCRIPT"]) 120 | 121 | XCTAssertThrowsError( 122 | try NetworkJSONHandlerTestDouble.json( 123 | with: DataTestDouble(), 124 | response: response 125 | ) 126 | ) { error in 127 | XCTAssertEqual( 128 | DataHandlerTestDouble.parameterData, 129 | DataTestDouble() 130 | ) 131 | XCTAssertIdentical( 132 | DataHandlerTestDouble.parameterResponse, 133 | response 134 | ) 135 | 136 | XCTAssertNil(JSONSerializationTestDouble.parameterData) 137 | XCTAssertNil(JSONSerializationTestDouble.parameterOptions) 138 | 139 | if let error = try? XCTUnwrap(error as? NetworkJSONHandlerTestDouble.Error) { 140 | XCTAssertEqual( 141 | error.code, 142 | .dataHandlerError 143 | ) 144 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 145 | XCTAssertIdentical( 146 | underlying, 147 | DataHandlerTestDouble.returnError 148 | ) 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | extension NetworkJSONHandlerTestCase { 156 | func testJSONSerializationError() { 157 | DataHandlerTestDouble.returnData = DataTestDouble() 158 | 159 | JSONSerializationTestDouble.returnJSON = nil 160 | 161 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "TEXT/JAVASCRIPT"]) 162 | 163 | XCTAssertThrowsError( 164 | try NetworkJSONHandlerTestDouble.json( 165 | with: DataTestDouble(), 166 | response: response 167 | ) 168 | ) { error in 169 | XCTAssertEqual( 170 | DataHandlerTestDouble.parameterData, 171 | DataTestDouble() 172 | ) 173 | XCTAssertIdentical( 174 | DataHandlerTestDouble.parameterResponse, 175 | response 176 | ) 177 | 178 | XCTAssertEqual( 179 | JSONSerializationTestDouble.parameterData, 180 | DataHandlerTestDouble.returnData 181 | ) 182 | XCTAssertEqual( 183 | JSONSerializationTestDouble.parameterOptions, 184 | [] 185 | ) 186 | 187 | if let error = try? XCTUnwrap(error as? NetworkJSONHandlerTestDouble.Error) { 188 | XCTAssertEqual( 189 | error.code, 190 | .jsonSerializationError 191 | ) 192 | if let underlying = try? XCTUnwrap(error.underlying as NSError?) { 193 | XCTAssertIdentical( 194 | underlying, 195 | JSONSerializationTestDouble.returnError 196 | ) 197 | } 198 | } 199 | } 200 | } 201 | } 202 | 203 | extension NetworkJSONHandlerTestCase { 204 | func testSuccess() { 205 | DataHandlerTestDouble.returnData = DataTestDouble() 206 | 207 | JSONSerializationTestDouble.returnJSON = NSObject() 208 | 209 | let response = HTTPURLResponseTestDouble(headerFields: ["CONTENT-TYPE": "TEXT/JAVASCRIPT"]) 210 | 211 | XCTAssertNoThrow( 212 | try { 213 | let json = try NetworkJSONHandlerTestDouble.json( 214 | with: DataTestDouble(), 215 | response: response 216 | ) 217 | 218 | XCTAssertEqual( 219 | DataHandlerTestDouble.parameterData, 220 | DataTestDouble() 221 | ) 222 | XCTAssertIdentical( 223 | DataHandlerTestDouble.parameterResponse, 224 | response 225 | ) 226 | 227 | XCTAssertEqual( 228 | JSONSerializationTestDouble.parameterData, 229 | DataHandlerTestDouble.returnData 230 | ) 231 | XCTAssertEqual( 232 | JSONSerializationTestDouble.parameterOptions, 233 | [] 234 | ) 235 | 236 | XCTAssertIdentical( 237 | json, 238 | JSONSerializationTestDouble.returnJSON 239 | ) 240 | }() 241 | ) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Albums.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1D0AACD627386E3D0065C4B8 /* NetworkDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACD527386E3D0065C4B8 /* NetworkDataHandler.swift */; }; 11 | 1D0AACD727386E3D0065C4B8 /* NetworkDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACD527386E3D0065C4B8 /* NetworkDataHandler.swift */; }; 12 | 1D0AACDA27386E470065C4B8 /* NetworkDataHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACD827386E470065C4B8 /* NetworkDataHandlerTests.swift */; }; 13 | 1D0AACDC27386EC90065C4B8 /* NetworkJSONHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACDB27386EC90065C4B8 /* NetworkJSONHandler.swift */; }; 14 | 1D0AACDD27386EC90065C4B8 /* NetworkJSONHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACDB27386EC90065C4B8 /* NetworkJSONHandler.swift */; }; 15 | 1D0AACDF27386ED70065C4B8 /* NetworkJSONHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACDE27386ED70065C4B8 /* NetworkJSONHandlerTests.swift */; }; 16 | 1D0AACE427386F2B0065C4B8 /* NetworkImageSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACE327386F2B0065C4B8 /* NetworkImageSource.m */; }; 17 | 1D0AACE527386F2B0065C4B8 /* NetworkImageSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACE327386F2B0065C4B8 /* NetworkImageSource.m */; }; 18 | 1D0AACE727386F380065C4B8 /* NetworkImageSourceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACE627386F380065C4B8 /* NetworkImageSourceTests.m */; }; 19 | 1D0AACE927386FCA0065C4B8 /* NetworkImageSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACE827386FCA0065C4B8 /* NetworkImageSerialization.swift */; }; 20 | 1D0AACEA27386FCA0065C4B8 /* NetworkImageSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACE827386FCA0065C4B8 /* NetworkImageSerialization.swift */; }; 21 | 1D0AACEC27386FDA0065C4B8 /* NetworkImageSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACEB27386FDA0065C4B8 /* NetworkImageSerializationTests.swift */; }; 22 | 1D0AACEE273870480065C4B8 /* NetworkImageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACED273870480065C4B8 /* NetworkImageHandler.swift */; }; 23 | 1D0AACEF273870480065C4B8 /* NetworkImageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACED273870480065C4B8 /* NetworkImageHandler.swift */; }; 24 | 1D0AACF1273870570065C4B8 /* NetworkImageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACF0273870570065C4B8 /* NetworkImageHandlerTests.swift */; }; 25 | 1D0AACF327387EA10065C4B8 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACF227387EA10065C4B8 /* NetworkSession.swift */; }; 26 | 1D0AACF427387EA10065C4B8 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACF227387EA10065C4B8 /* NetworkSession.swift */; }; 27 | 1D0AACF627387EAD0065C4B8 /* NetworkSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACF527387EAD0065C4B8 /* NetworkSessionTests.swift */; }; 28 | 1D0AACF827387EFA0065C4B8 /* NetworkJSONOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACF727387EFA0065C4B8 /* NetworkJSONOperation.swift */; }; 29 | 1D0AACF927387EFA0065C4B8 /* NetworkJSONOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACF727387EFA0065C4B8 /* NetworkJSONOperation.swift */; }; 30 | 1D0AACFB27387F060065C4B8 /* NetworkJSONOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACFA27387F060065C4B8 /* NetworkJSONOperationTests.swift */; }; 31 | 1D0AACFD273880870065C4B8 /* NetworkImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACFC273880870065C4B8 /* NetworkImageOperation.swift */; }; 32 | 1D0AACFE273880870065C4B8 /* NetworkImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACFC273880870065C4B8 /* NetworkImageOperation.swift */; }; 33 | 1D0AAD00273880940065C4B8 /* NetworkImageOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AACFF273880940065C4B8 /* NetworkImageOperationTests.swift */; }; 34 | 1D0AAD02273881620065C4B8 /* AlbumsListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD01273881620065C4B8 /* AlbumsListModel.swift */; }; 35 | 1D0AAD03273881620065C4B8 /* AlbumsListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD01273881620065C4B8 /* AlbumsListModel.swift */; }; 36 | 1D0AAD052738816F0065C4B8 /* AlbumsListModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD042738816E0065C4B8 /* AlbumsListModelTests.swift */; }; 37 | 1D0AAD072738818E0065C4B8 /* Albums.json in Resources */ = {isa = PBXBuildFile; fileRef = 1D0AAD062738818D0065C4B8 /* Albums.json */; }; 38 | 1D0AAD09273881D00065C4B8 /* AlbumsListRowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD08273881D00065C4B8 /* AlbumsListRowModel.swift */; }; 39 | 1D0AAD0A273881D00065C4B8 /* AlbumsListRowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD08273881D00065C4B8 /* AlbumsListRowModel.swift */; }; 40 | 1D0AAD0C273881DC0065C4B8 /* AlbumsListRowModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD0B273881DC0065C4B8 /* AlbumsListRowModelTests.swift */; }; 41 | 1D0AAD0E2738823A0065C4B8 /* AlbumsListRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD0D2738823A0065C4B8 /* AlbumsListRowView.swift */; }; 42 | 1D0AAD10273882750065C4B8 /* AlbumsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0AAD0F273882750065C4B8 /* AlbumsListView.swift */; }; 43 | AE657E3B27068BE40032C442 /* AlbumsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE657E3A27068BE40032C442 /* AlbumsApp.swift */; }; 44 | AE657E4C27068BE70032C442 /* AlbumsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE657E4B27068BE70032C442 /* AlbumsTests.swift */; }; 45 | /* End PBXBuildFile section */ 46 | 47 | /* Begin PBXContainerItemProxy section */ 48 | AE657E4827068BE70032C442 /* PBXContainerItemProxy */ = { 49 | isa = PBXContainerItemProxy; 50 | containerPortal = AE657E2F27068BE40032C442 /* Project object */; 51 | proxyType = 1; 52 | remoteGlobalIDString = AE657E3627068BE40032C442; 53 | remoteInfo = Albums; 54 | }; 55 | /* End PBXContainerItemProxy section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | 1D0AACD327386DA20065C4B8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 59 | 1D0AACD427386DA20065C4B8 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 60 | 1D0AACD527386E3D0065C4B8 /* NetworkDataHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkDataHandler.swift; sourceTree = ""; }; 61 | 1D0AACD827386E470065C4B8 /* NetworkDataHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkDataHandlerTests.swift; sourceTree = ""; }; 62 | 1D0AACDB27386EC90065C4B8 /* NetworkJSONHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkJSONHandler.swift; sourceTree = ""; }; 63 | 1D0AACDE27386ED70065C4B8 /* NetworkJSONHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkJSONHandlerTests.swift; sourceTree = ""; }; 64 | 1D0AACE027386F2B0065C4B8 /* Albums-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Albums-Bridging-Header.h"; sourceTree = ""; }; 65 | 1D0AACE227386F2B0065C4B8 /* NetworkImageSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetworkImageSource.h; sourceTree = ""; }; 66 | 1D0AACE327386F2B0065C4B8 /* NetworkImageSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NetworkImageSource.m; sourceTree = ""; }; 67 | 1D0AACE627386F380065C4B8 /* NetworkImageSourceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NetworkImageSourceTests.m; sourceTree = ""; }; 68 | 1D0AACE827386FCA0065C4B8 /* NetworkImageSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageSerialization.swift; sourceTree = ""; }; 69 | 1D0AACEB27386FDA0065C4B8 /* NetworkImageSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageSerializationTests.swift; sourceTree = ""; }; 70 | 1D0AACED273870480065C4B8 /* NetworkImageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageHandler.swift; sourceTree = ""; }; 71 | 1D0AACF0273870570065C4B8 /* NetworkImageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageHandlerTests.swift; sourceTree = ""; }; 72 | 1D0AACF227387EA10065C4B8 /* NetworkSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = ""; }; 73 | 1D0AACF527387EAD0065C4B8 /* NetworkSessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSessionTests.swift; sourceTree = ""; }; 74 | 1D0AACF727387EFA0065C4B8 /* NetworkJSONOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkJSONOperation.swift; sourceTree = ""; }; 75 | 1D0AACFA27387F060065C4B8 /* NetworkJSONOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkJSONOperationTests.swift; sourceTree = ""; }; 76 | 1D0AACFC273880870065C4B8 /* NetworkImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageOperation.swift; sourceTree = ""; }; 77 | 1D0AACFF273880940065C4B8 /* NetworkImageOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageOperationTests.swift; sourceTree = ""; }; 78 | 1D0AAD01273881620065C4B8 /* AlbumsListModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumsListModel.swift; sourceTree = ""; }; 79 | 1D0AAD042738816E0065C4B8 /* AlbumsListModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumsListModelTests.swift; sourceTree = ""; }; 80 | 1D0AAD062738818D0065C4B8 /* Albums.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Albums.json; sourceTree = ""; }; 81 | 1D0AAD08273881D00065C4B8 /* AlbumsListRowModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumsListRowModel.swift; sourceTree = ""; }; 82 | 1D0AAD0B273881DC0065C4B8 /* AlbumsListRowModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumsListRowModelTests.swift; sourceTree = ""; }; 83 | 1D0AAD0D2738823A0065C4B8 /* AlbumsListRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumsListRowView.swift; sourceTree = ""; }; 84 | 1D0AAD0F273882750065C4B8 /* AlbumsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumsListView.swift; sourceTree = ""; }; 85 | AE657E3727068BE40032C442 /* Albums.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Albums.app; sourceTree = BUILT_PRODUCTS_DIR; }; 86 | AE657E3A27068BE40032C442 /* AlbumsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumsApp.swift; sourceTree = ""; }; 87 | AE657E4727068BE70032C442 /* AlbumsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AlbumsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 88 | AE657E4B27068BE70032C442 /* AlbumsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumsTests.swift; sourceTree = ""; }; 89 | /* End PBXFileReference section */ 90 | 91 | /* Begin PBXFrameworksBuildPhase section */ 92 | AE657E3427068BE40032C442 /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | AE657E4427068BE70032C442 /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | AE657E2E27068BE40032C442 = { 110 | isa = PBXGroup; 111 | children = ( 112 | AE657E3927068BE40032C442 /* Albums */, 113 | AE657E4A27068BE70032C442 /* AlbumsTests */, 114 | 1D0AACD427386DA20065C4B8 /* LICENSE.md */, 115 | AE657E3827068BE40032C442 /* Products */, 116 | 1D0AACD327386DA20065C4B8 /* README.md */, 117 | ); 118 | sourceTree = ""; 119 | }; 120 | AE657E3827068BE40032C442 /* Products */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | AE657E3727068BE40032C442 /* Albums.app */, 124 | AE657E4727068BE70032C442 /* AlbumsTests.xctest */, 125 | ); 126 | name = Products; 127 | sourceTree = ""; 128 | }; 129 | AE657E3927068BE40032C442 /* Albums */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 1D0AACE027386F2B0065C4B8 /* Albums-Bridging-Header.h */, 133 | AE657E3A27068BE40032C442 /* AlbumsApp.swift */, 134 | 1D0AAD01273881620065C4B8 /* AlbumsListModel.swift */, 135 | 1D0AAD08273881D00065C4B8 /* AlbumsListRowModel.swift */, 136 | 1D0AAD0D2738823A0065C4B8 /* AlbumsListRowView.swift */, 137 | 1D0AAD0F273882750065C4B8 /* AlbumsListView.swift */, 138 | 1D0AACD527386E3D0065C4B8 /* NetworkDataHandler.swift */, 139 | 1D0AACED273870480065C4B8 /* NetworkImageHandler.swift */, 140 | 1D0AACFC273880870065C4B8 /* NetworkImageOperation.swift */, 141 | 1D0AACE827386FCA0065C4B8 /* NetworkImageSerialization.swift */, 142 | 1D0AACE227386F2B0065C4B8 /* NetworkImageSource.h */, 143 | 1D0AACE327386F2B0065C4B8 /* NetworkImageSource.m */, 144 | 1D0AACDB27386EC90065C4B8 /* NetworkJSONHandler.swift */, 145 | 1D0AACF727387EFA0065C4B8 /* NetworkJSONOperation.swift */, 146 | 1D0AACF227387EA10065C4B8 /* NetworkSession.swift */, 147 | ); 148 | path = Albums; 149 | sourceTree = ""; 150 | }; 151 | AE657E4A27068BE70032C442 /* AlbumsTests */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 1D0AAD062738818D0065C4B8 /* Albums.json */, 155 | 1D0AAD042738816E0065C4B8 /* AlbumsListModelTests.swift */, 156 | 1D0AAD0B273881DC0065C4B8 /* AlbumsListRowModelTests.swift */, 157 | AE657E4B27068BE70032C442 /* AlbumsTests.swift */, 158 | 1D0AACD827386E470065C4B8 /* NetworkDataHandlerTests.swift */, 159 | 1D0AACF0273870570065C4B8 /* NetworkImageHandlerTests.swift */, 160 | 1D0AACFF273880940065C4B8 /* NetworkImageOperationTests.swift */, 161 | 1D0AACEB27386FDA0065C4B8 /* NetworkImageSerializationTests.swift */, 162 | 1D0AACE627386F380065C4B8 /* NetworkImageSourceTests.m */, 163 | 1D0AACDE27386ED70065C4B8 /* NetworkJSONHandlerTests.swift */, 164 | 1D0AACFA27387F060065C4B8 /* NetworkJSONOperationTests.swift */, 165 | 1D0AACF527387EAD0065C4B8 /* NetworkSessionTests.swift */, 166 | ); 167 | path = AlbumsTests; 168 | sourceTree = ""; 169 | }; 170 | /* End PBXGroup section */ 171 | 172 | /* Begin PBXNativeTarget section */ 173 | AE657E3627068BE40032C442 /* Albums */ = { 174 | isa = PBXNativeTarget; 175 | buildConfigurationList = AE657E5B27068BE70032C442 /* Build configuration list for PBXNativeTarget "Albums" */; 176 | buildPhases = ( 177 | AE657E3327068BE40032C442 /* Sources */, 178 | AE657E3427068BE40032C442 /* Frameworks */, 179 | AE657E3527068BE40032C442 /* Resources */, 180 | ); 181 | buildRules = ( 182 | ); 183 | dependencies = ( 184 | ); 185 | name = Albums; 186 | productName = Albums; 187 | productReference = AE657E3727068BE40032C442 /* Albums.app */; 188 | productType = "com.apple.product-type.application"; 189 | }; 190 | AE657E4627068BE70032C442 /* AlbumsTests */ = { 191 | isa = PBXNativeTarget; 192 | buildConfigurationList = AE657E5E27068BE70032C442 /* Build configuration list for PBXNativeTarget "AlbumsTests" */; 193 | buildPhases = ( 194 | AE657E4327068BE70032C442 /* Sources */, 195 | AE657E4427068BE70032C442 /* Frameworks */, 196 | AE657E4527068BE70032C442 /* Resources */, 197 | ); 198 | buildRules = ( 199 | ); 200 | dependencies = ( 201 | AE657E4927068BE70032C442 /* PBXTargetDependency */, 202 | ); 203 | name = AlbumsTests; 204 | productName = AlbumsTests; 205 | productReference = AE657E4727068BE70032C442 /* AlbumsTests.xctest */; 206 | productType = "com.apple.product-type.bundle.unit-test"; 207 | }; 208 | /* End PBXNativeTarget section */ 209 | 210 | /* Begin PBXProject section */ 211 | AE657E2F27068BE40032C442 /* Project object */ = { 212 | isa = PBXProject; 213 | attributes = { 214 | BuildIndependentTargetsInParallel = 1; 215 | LastSwiftUpdateCheck = 1300; 216 | LastUpgradeCheck = 1300; 217 | TargetAttributes = { 218 | AE657E3627068BE40032C442 = { 219 | CreatedOnToolsVersion = 13.0; 220 | LastSwiftMigration = 1310; 221 | }; 222 | AE657E4627068BE70032C442 = { 223 | CreatedOnToolsVersion = 13.0; 224 | LastSwiftMigration = 1310; 225 | TestTargetID = AE657E3627068BE40032C442; 226 | }; 227 | }; 228 | }; 229 | buildConfigurationList = AE657E3227068BE40032C442 /* Build configuration list for PBXProject "Albums" */; 230 | compatibilityVersion = "Xcode 13.0"; 231 | developmentRegion = en; 232 | hasScannedForEncodings = 0; 233 | knownRegions = ( 234 | en, 235 | Base, 236 | ); 237 | mainGroup = AE657E2E27068BE40032C442; 238 | productRefGroup = AE657E3827068BE40032C442 /* Products */; 239 | projectDirPath = ""; 240 | projectRoot = ""; 241 | targets = ( 242 | AE657E3627068BE40032C442 /* Albums */, 243 | AE657E4627068BE70032C442 /* AlbumsTests */, 244 | ); 245 | }; 246 | /* End PBXProject section */ 247 | 248 | /* Begin PBXResourcesBuildPhase section */ 249 | AE657E3527068BE40032C442 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | AE657E4527068BE70032C442 /* Resources */ = { 257 | isa = PBXResourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | 1D0AAD072738818E0065C4B8 /* Albums.json in Resources */, 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | /* End PBXResourcesBuildPhase section */ 265 | 266 | /* Begin PBXSourcesBuildPhase section */ 267 | AE657E3327068BE40032C442 /* Sources */ = { 268 | isa = PBXSourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 1D0AAD09273881D00065C4B8 /* AlbumsListRowModel.swift in Sources */, 272 | 1D0AAD0E2738823A0065C4B8 /* AlbumsListRowView.swift in Sources */, 273 | 1D0AACF327387EA10065C4B8 /* NetworkSession.swift in Sources */, 274 | AE657E3B27068BE40032C442 /* AlbumsApp.swift in Sources */, 275 | 1D0AACFD273880870065C4B8 /* NetworkImageOperation.swift in Sources */, 276 | 1D0AAD02273881620065C4B8 /* AlbumsListModel.swift in Sources */, 277 | 1D0AACF827387EFA0065C4B8 /* NetworkJSONOperation.swift in Sources */, 278 | 1D0AACEE273870480065C4B8 /* NetworkImageHandler.swift in Sources */, 279 | 1D0AACE427386F2B0065C4B8 /* NetworkImageSource.m in Sources */, 280 | 1D0AACD627386E3D0065C4B8 /* NetworkDataHandler.swift in Sources */, 281 | 1D0AACE927386FCA0065C4B8 /* NetworkImageSerialization.swift in Sources */, 282 | 1D0AAD10273882750065C4B8 /* AlbumsListView.swift in Sources */, 283 | 1D0AACDC27386EC90065C4B8 /* NetworkJSONHandler.swift in Sources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | AE657E4327068BE70032C442 /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | 1D0AAD0A273881D00065C4B8 /* AlbumsListRowModel.swift in Sources */, 292 | 1D0AACDA27386E470065C4B8 /* NetworkDataHandlerTests.swift in Sources */, 293 | 1D0AACDD27386EC90065C4B8 /* NetworkJSONHandler.swift in Sources */, 294 | 1D0AACF1273870570065C4B8 /* NetworkImageHandlerTests.swift in Sources */, 295 | 1D0AAD03273881620065C4B8 /* AlbumsListModel.swift in Sources */, 296 | AE657E4C27068BE70032C442 /* AlbumsTests.swift in Sources */, 297 | 1D0AACFE273880870065C4B8 /* NetworkImageOperation.swift in Sources */, 298 | 1D0AAD052738816F0065C4B8 /* AlbumsListModelTests.swift in Sources */, 299 | 1D0AAD00273880940065C4B8 /* NetworkImageOperationTests.swift in Sources */, 300 | 1D0AACEF273870480065C4B8 /* NetworkImageHandler.swift in Sources */, 301 | 1D0AACE527386F2B0065C4B8 /* NetworkImageSource.m in Sources */, 302 | 1D0AACEC27386FDA0065C4B8 /* NetworkImageSerializationTests.swift in Sources */, 303 | 1D0AACDF27386ED70065C4B8 /* NetworkJSONHandlerTests.swift in Sources */, 304 | 1D0AACEA27386FCA0065C4B8 /* NetworkImageSerialization.swift in Sources */, 305 | 1D0AAD0C273881DC0065C4B8 /* AlbumsListRowModelTests.swift in Sources */, 306 | 1D0AACF627387EAD0065C4B8 /* NetworkSessionTests.swift in Sources */, 307 | 1D0AACF927387EFA0065C4B8 /* NetworkJSONOperation.swift in Sources */, 308 | 1D0AACF427387EA10065C4B8 /* NetworkSession.swift in Sources */, 309 | 1D0AACD727386E3D0065C4B8 /* NetworkDataHandler.swift in Sources */, 310 | 1D0AACE727386F380065C4B8 /* NetworkImageSourceTests.m in Sources */, 311 | 1D0AACFB27387F060065C4B8 /* NetworkJSONOperationTests.swift in Sources */, 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | /* End PBXSourcesBuildPhase section */ 316 | 317 | /* Begin PBXTargetDependency section */ 318 | AE657E4927068BE70032C442 /* PBXTargetDependency */ = { 319 | isa = PBXTargetDependency; 320 | target = AE657E3627068BE40032C442 /* Albums */; 321 | targetProxy = AE657E4827068BE70032C442 /* PBXContainerItemProxy */; 322 | }; 323 | /* End PBXTargetDependency section */ 324 | 325 | /* Begin XCBuildConfiguration section */ 326 | AE657E5927068BE70032C442 /* Debug */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_ANALYZER_NONNULL = YES; 331 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 332 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 333 | CLANG_CXX_LIBRARY = "libc++"; 334 | CLANG_ENABLE_MODULES = YES; 335 | CLANG_ENABLE_OBJC_ARC = YES; 336 | CLANG_ENABLE_OBJC_WEAK = YES; 337 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 338 | CLANG_WARN_BOOL_CONVERSION = YES; 339 | CLANG_WARN_COMMA = YES; 340 | CLANG_WARN_CONSTANT_CONVERSION = YES; 341 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 342 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 343 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 344 | CLANG_WARN_EMPTY_BODY = YES; 345 | CLANG_WARN_ENUM_CONVERSION = YES; 346 | CLANG_WARN_INFINITE_RECURSION = YES; 347 | CLANG_WARN_INT_CONVERSION = YES; 348 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 350 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 352 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 353 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 354 | CLANG_WARN_STRICT_PROTOTYPES = YES; 355 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 356 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 357 | CLANG_WARN_UNREACHABLE_CODE = YES; 358 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 359 | COPY_PHASE_STRIP = NO; 360 | DEBUG_INFORMATION_FORMAT = dwarf; 361 | ENABLE_STRICT_OBJC_MSGSEND = YES; 362 | ENABLE_TESTABILITY = YES; 363 | GCC_C_LANGUAGE_STANDARD = gnu11; 364 | GCC_DYNAMIC_NO_PIC = NO; 365 | GCC_NO_COMMON_BLOCKS = YES; 366 | GCC_OPTIMIZATION_LEVEL = 0; 367 | GCC_PREPROCESSOR_DEFINITIONS = ( 368 | "DEBUG=1", 369 | "$(inherited)", 370 | ); 371 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 372 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 373 | GCC_WARN_UNDECLARED_SELECTOR = YES; 374 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 375 | GCC_WARN_UNUSED_FUNCTION = YES; 376 | GCC_WARN_UNUSED_VARIABLE = YES; 377 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 378 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 379 | MTL_FAST_MATH = YES; 380 | ONLY_ACTIVE_ARCH = YES; 381 | SDKROOT = iphoneos; 382 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 383 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 384 | }; 385 | name = Debug; 386 | }; 387 | AE657E5A27068BE70032C442 /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ALWAYS_SEARCH_USER_PATHS = NO; 391 | CLANG_ANALYZER_NONNULL = YES; 392 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 393 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 394 | CLANG_CXX_LIBRARY = "libc++"; 395 | CLANG_ENABLE_MODULES = YES; 396 | CLANG_ENABLE_OBJC_ARC = YES; 397 | CLANG_ENABLE_OBJC_WEAK = YES; 398 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 399 | CLANG_WARN_BOOL_CONVERSION = YES; 400 | CLANG_WARN_COMMA = YES; 401 | CLANG_WARN_CONSTANT_CONVERSION = YES; 402 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 403 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 404 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 405 | CLANG_WARN_EMPTY_BODY = YES; 406 | CLANG_WARN_ENUM_CONVERSION = YES; 407 | CLANG_WARN_INFINITE_RECURSION = YES; 408 | CLANG_WARN_INT_CONVERSION = YES; 409 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 410 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 411 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 412 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 413 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 414 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 415 | CLANG_WARN_STRICT_PROTOTYPES = YES; 416 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 417 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 418 | CLANG_WARN_UNREACHABLE_CODE = YES; 419 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 420 | COPY_PHASE_STRIP = NO; 421 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 422 | ENABLE_NS_ASSERTIONS = NO; 423 | ENABLE_STRICT_OBJC_MSGSEND = YES; 424 | GCC_C_LANGUAGE_STANDARD = gnu11; 425 | GCC_NO_COMMON_BLOCKS = YES; 426 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 427 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 428 | GCC_WARN_UNDECLARED_SELECTOR = YES; 429 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 430 | GCC_WARN_UNUSED_FUNCTION = YES; 431 | GCC_WARN_UNUSED_VARIABLE = YES; 432 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 433 | MTL_ENABLE_DEBUG_INFO = NO; 434 | MTL_FAST_MATH = YES; 435 | SDKROOT = iphoneos; 436 | SWIFT_COMPILATION_MODE = wholemodule; 437 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 438 | VALIDATE_PRODUCT = YES; 439 | }; 440 | name = Release; 441 | }; 442 | AE657E5C27068BE70032C442 /* Debug */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 446 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 447 | CLANG_ENABLE_MODULES = YES; 448 | CODE_SIGN_STYLE = Automatic; 449 | CURRENT_PROJECT_VERSION = 1; 450 | DEVELOPMENT_ASSET_PATHS = ""; 451 | ENABLE_PREVIEWS = YES; 452 | GENERATE_INFOPLIST_FILE = YES; 453 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 454 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 455 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 456 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 457 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 458 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 459 | LD_RUNPATH_SEARCH_PATHS = ( 460 | "$(inherited)", 461 | "@executable_path/Frameworks", 462 | ); 463 | MARKETING_VERSION = 1.0; 464 | PRODUCT_BUNDLE_IDENTIFIER = com.northbronson.Albums; 465 | PRODUCT_NAME = "$(TARGET_NAME)"; 466 | SWIFT_EMIT_LOC_STRINGS = YES; 467 | SWIFT_OBJC_BRIDGING_HEADER = "Albums/Albums-Bridging-Header.h"; 468 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Debug; 473 | }; 474 | AE657E5D27068BE70032C442 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 479 | CLANG_ENABLE_MODULES = YES; 480 | CODE_SIGN_STYLE = Automatic; 481 | CURRENT_PROJECT_VERSION = 1; 482 | DEVELOPMENT_ASSET_PATHS = ""; 483 | ENABLE_PREVIEWS = YES; 484 | GENERATE_INFOPLIST_FILE = YES; 485 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 486 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 487 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 488 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 489 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 490 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 491 | LD_RUNPATH_SEARCH_PATHS = ( 492 | "$(inherited)", 493 | "@executable_path/Frameworks", 494 | ); 495 | MARKETING_VERSION = 1.0; 496 | PRODUCT_BUNDLE_IDENTIFIER = com.northbronson.Albums; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | SWIFT_EMIT_LOC_STRINGS = YES; 499 | SWIFT_OBJC_BRIDGING_HEADER = "Albums/Albums-Bridging-Header.h"; 500 | SWIFT_VERSION = 5.0; 501 | TARGETED_DEVICE_FAMILY = "1,2"; 502 | }; 503 | name = Release; 504 | }; 505 | AE657E5F27068BE70032C442 /* Debug */ = { 506 | isa = XCBuildConfiguration; 507 | buildSettings = { 508 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 509 | BUNDLE_LOADER = "$(TEST_HOST)"; 510 | CLANG_ENABLE_MODULES = YES; 511 | CODE_SIGN_STYLE = Automatic; 512 | CURRENT_PROJECT_VERSION = 1; 513 | GENERATE_INFOPLIST_FILE = YES; 514 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 515 | LD_RUNPATH_SEARCH_PATHS = ( 516 | "$(inherited)", 517 | "@executable_path/Frameworks", 518 | "@loader_path/Frameworks", 519 | ); 520 | MARKETING_VERSION = 1.0; 521 | PRODUCT_BUNDLE_IDENTIFIER = com.northbronson.AlbumsTests; 522 | PRODUCT_NAME = "$(TARGET_NAME)"; 523 | SWIFT_EMIT_LOC_STRINGS = NO; 524 | SWIFT_OBJC_BRIDGING_HEADER = "Albums/Albums-Bridging-Header.h"; 525 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 526 | SWIFT_VERSION = 5.0; 527 | TARGETED_DEVICE_FAMILY = "1,2"; 528 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Albums.app/Albums"; 529 | }; 530 | name = Debug; 531 | }; 532 | AE657E6027068BE70032C442 /* Release */ = { 533 | isa = XCBuildConfiguration; 534 | buildSettings = { 535 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 536 | BUNDLE_LOADER = "$(TEST_HOST)"; 537 | CLANG_ENABLE_MODULES = YES; 538 | CODE_SIGN_STYLE = Automatic; 539 | CURRENT_PROJECT_VERSION = 1; 540 | GENERATE_INFOPLIST_FILE = YES; 541 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 542 | LD_RUNPATH_SEARCH_PATHS = ( 543 | "$(inherited)", 544 | "@executable_path/Frameworks", 545 | "@loader_path/Frameworks", 546 | ); 547 | MARKETING_VERSION = 1.0; 548 | PRODUCT_BUNDLE_IDENTIFIER = com.northbronson.AlbumsTests; 549 | PRODUCT_NAME = "$(TARGET_NAME)"; 550 | SWIFT_EMIT_LOC_STRINGS = NO; 551 | SWIFT_OBJC_BRIDGING_HEADER = "Albums/Albums-Bridging-Header.h"; 552 | SWIFT_VERSION = 5.0; 553 | TARGETED_DEVICE_FAMILY = "1,2"; 554 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Albums.app/Albums"; 555 | }; 556 | name = Release; 557 | }; 558 | /* End XCBuildConfiguration section */ 559 | 560 | /* Begin XCConfigurationList section */ 561 | AE657E3227068BE40032C442 /* Build configuration list for PBXProject "Albums" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | AE657E5927068BE70032C442 /* Debug */, 565 | AE657E5A27068BE70032C442 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | AE657E5B27068BE70032C442 /* Build configuration list for PBXNativeTarget "Albums" */ = { 571 | isa = XCConfigurationList; 572 | buildConfigurations = ( 573 | AE657E5C27068BE70032C442 /* Debug */, 574 | AE657E5D27068BE70032C442 /* Release */, 575 | ); 576 | defaultConfigurationIsVisible = 0; 577 | defaultConfigurationName = Release; 578 | }; 579 | AE657E5E27068BE70032C442 /* Build configuration list for PBXNativeTarget "AlbumsTests" */ = { 580 | isa = XCConfigurationList; 581 | buildConfigurations = ( 582 | AE657E5F27068BE70032C442 /* Debug */, 583 | AE657E6027068BE70032C442 /* Release */, 584 | ); 585 | defaultConfigurationIsVisible = 0; 586 | defaultConfigurationName = Release; 587 | }; 588 | /* End XCConfigurationList section */ 589 | }; 590 | rootObject = AE657E2F27068BE40032C442 /* Project object */; 591 | } 592 | --------------------------------------------------------------------------------