├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── UnifiedBlurHash.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── UnifiedBlurHash │ ├── Math.swift │ ├── Resources │ └── sunflower.jpg │ ├── SwiftUI.Image+BlurHash.swift │ ├── UnifiedBlurHash.swift │ ├── UnifiedImage+Decode.swift │ ├── UnifiedImage+Encode.swift │ ├── UnifiedImage+Scale.swift │ └── UnifiedImage.swift ├── Tests └── UnifiedBlurHashTests │ ├── UnifiedBlurHashTests.swift │ ├── UnifiedImageResizingTests.swift │ └── Utilities.swift └── demo ├── demo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── demo.xcscheme │ └── watchOS-demo Watch App.xcscheme ├── demo ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── sunflower.imageset │ │ ├── Contents.json │ │ └── sunflower.jpg ├── ContentView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── demo.entitlements └── demoApp.swift ├── demoTests └── demoTests.swift ├── demoUITests ├── demoUITests.swift └── demoUITestsLaunchTests.swift ├── watchOS-demo Watch App ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── sunflower.imageset │ │ ├── Contents.json │ │ └── pexels-lil-artsy-1624076-2.jpg ├── ContentView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── watchOS_demoApp.swift ├── watchOS-demo Watch AppTests └── watchOS_demo_Watch_AppTests.swift └── watchOS-demo Watch AppUITests ├── watchOS_demo_Watch_AppUITests.swift └── watchOS_demo_Watch_AppUITestsLaunchTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift Build and Test 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | branches: [main] 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | name: Build and Test 20 | runs-on: macos-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: List available Xcode versions 24 | run: ls /Applications | grep Xcode 25 | - name: Set up Xcode version 26 | run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer 27 | - name: Show current version of Xcode 28 | run: xcodebuild -version 29 | - name: Show current version of Swift 30 | run: swift -version 31 | - name: Build 32 | run: swift build -v 33 | - name: Run tests 34 | run: swift test -v 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/UnifiedBlurHash.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 iankoex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "UnifiedBlurHash", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "UnifiedBlurHash", 12 | targets: ["UnifiedBlurHash"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "UnifiedBlurHash", 23 | dependencies: [], 24 | resources: [ 25 | .process("Resources") 26 | ] 27 | ), 28 | .testTarget( 29 | name: "UnifiedBlurHashTests", 30 | dependencies: ["UnifiedBlurHash"] 31 | ), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnifiedBlurHash 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fiankoex%2FUnifiedBlurHash%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/iankoex/UnifiedBlurHash) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fiankoex%2FUnifiedBlurHash%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/iankoex/UnifiedBlurHash) 5 | [![Swift Build and Test](https://github.com/iankoex/UnifiedBlurHash/actions/workflows/swift.yml/badge.svg)](https://github.com/iankoex/UnifiedBlurHash/actions/workflows/swift.yml) 6 | 7 | Extensions of `UIImage` and `NSImage` to encode and decode blur hashes, based on [Wolt's Implementation](https://github.com/woltapp/blurhash) for Swift (iOS). 8 | 9 | Based on [Mortenjust's Implementation](https://github.com/mortenjust/Blurhash-macos.git) for macOS 10 | 11 | Screenshot 12 | 13 | ![Screenshot](https://user-images.githubusercontent.com/30172987/207150953-d1177cad-da76-40a8-bfdc-4c05f47ce5a0.png) 14 | 15 | # About [BlurHash](http://blurha.sh) 16 | 17 | BlurHash is a compact representation of a placeholder for an image. 18 | 19 | Learn More from the [BlurHash Repo](https://github.com/woltapp/blurhash.git) 20 | 21 | ### Why would you want this? 22 | 23 | Does your designer cry every time you load their beautifully designed screen, and it is full of empty boxes because all the 24 | images have not loaded yet? Does your database engineer cry when you want to solve this by trying to cram little thumbnail 25 | images into your data to show as placeholders? 26 | 27 | BlurHash will solve your problems! How? Like this: 28 | 29 | ![WhyBlurHash](https://user-images.githubusercontent.com/30172987/207242393-5446ece1-7d55-412f-92b6-0a7c4cf36860.png) 30 | 31 | 32 | You can also see nice examples and try it out yourself at [blurha.sh](http://blurha.sh/)! 33 | Learn More from the [BlurHash Repo](https://github.com/woltapp/blurhash.git) 34 | 35 | # Usage 36 | There are two ways you can use this package: 37 | 1. Using Async Helpers 38 | ```swift 39 | // Encoding 40 | let blurHashStr = await UnifiedBlurHash.getBlurHashString(from: image) 41 | 42 | // Decoding 43 | let image = await UnifiedBlurHash.getUnifiedImage(from: imageString) 44 | 45 | ``` 46 | 47 | 2. Using Methods Directly 48 | 49 | ```swift 50 | 51 | // scale down for performance then encode 52 | let blurHashStr = unifiedImage.small?.blurHash(numberOfComponents: (4,4)) 53 | 54 | // Decoding. Use small size and resize in UI for performance 55 | let image = UnifiedImage(blurHash: blurHashString, size: .init(width: 32, height: 32)) 56 | 57 | ``` 58 | 59 | # SwiftUI Examples 60 | 61 | ```swift 62 | 63 | import SwiftUI 64 | import UnifiedBlurHash 65 | 66 | struct ContentView: View { 67 | @State private var imageString = "LVN^Odxa?WNa-;WB£,WBs;baR*af" 68 | @State private var image: UnifiedImage? = nil 69 | 70 | var body: some View { 71 | VStack(spacing: 20) { 72 | 73 | Button("Encode", action: encode) 74 | 75 | Button("Decode", action: decode) 76 | 77 | if image != nil { 78 | Image(unifiedImage: image!) 79 | .resizable() 80 | } 81 | } 82 | .padding(.horizontal) 83 | } 84 | 85 | private func encode() { 86 | Task { 87 | let image = UnifiedImage(named: "sunflower") 88 | guard let image = image else { 89 | return 90 | } 91 | let str = await UnifiedBlurHash.getBlurHashString(from: image) 92 | guard let str = str else { 93 | return 94 | } 95 | DispatchQueue.main.async { 96 | self.imageString = str 97 | } 98 | } 99 | } 100 | 101 | private func decode() { 102 | Task { 103 | let image = await UnifiedBlurHash.getUnifiedImage(from: imageString) 104 | guard let image = image else { 105 | return 106 | } 107 | DispatchQueue.main.async { 108 | self.image = image 109 | } 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | or 116 | 117 | ```swift 118 | 119 | import SwiftUI 120 | import UnifiedBlurHash 121 | 122 | struct ContentView: View { 123 | var blurHash: String = "LVN^Odxa?WNa-;WB£,WBs;baR*af" 124 | 125 | var body: some View { 126 | Image(blurHash: blurHash)? 127 | .resizable() 128 | } 129 | } 130 | 131 | ``` 132 | Under the hood `UnifiedImage` is just `UIImage` or `NSImage` depending on the platform. 133 | 134 | # License 135 | MIT 136 | -------------------------------------------------------------------------------- /Sources/UnifiedBlurHash/Math.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Math.swift 3 | // UnifiedBlurHash 4 | // 5 | // Created by ian on 03/05/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Math { 11 | @inline(__always) 12 | static func signPow(_ value: Float, _ exp: Float) -> Float { 13 | return copysign(pow(abs(value), exp), value) 14 | } 15 | 16 | @inline(__always) 17 | static func linearTosRGB(_ value: Float) -> Int { 18 | let v = max(0, min(1, value)) 19 | if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } 20 | else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } 21 | } 22 | 23 | @inline(__always) 24 | static func sRGBToLinear(_ value: Type) -> Float { 25 | let v = Float(Int64(value)) / 255 26 | if v <= 0.04045 { return v / 12.92 } 27 | else { return pow((v + 0.055) / 1.055, 2.4) } 28 | } 29 | 30 | // Precomputed encode characters for faster access 31 | static let encodeCharacters: [Character] = { 32 | return Array("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~") 33 | }() 34 | } 35 | -------------------------------------------------------------------------------- /Sources/UnifiedBlurHash/Resources/sunflower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iankoex/UnifiedBlurHash/675d79835d6338e3fd4a314ef3220ae41f7eb6d4/Sources/UnifiedBlurHash/Resources/sunflower.jpg -------------------------------------------------------------------------------- /Sources/UnifiedBlurHash/SwiftUI.Image+BlurHash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUI.Image+BlurHash.swift 3 | // 4 | // 5 | // Created by Ian on 13/12/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A convenience initializer for creating a SwiftUI `Image` from a [BlurHash](https://blurha.sh/) string. 11 | /// 12 | /// This initializer attempts to decode a BlurHash string into an image of a specified size and with a punch factor for contrast enhancement. 13 | /// 14 | /// - Parameters: 15 | /// - blurHash: A non-empty `String` containing the BlurHash to decode. The string should conform to the BlurHash specification. 16 | /// - size: A `CGSize` specifying the desired dimensions of the resulting image. Defaults to 32x32 pixels if not specified. 17 | /// - punch: A `Float` used to adjust the contrast of the decoded image. Values greater than 1.0 increase contrast. Default is 1. 18 | /// 19 | /// - Returns: An optional `Image`. Returns `nil` if the BlurHash string is invalid or decoding fails. 20 | /// 21 | /// - Important: 22 | /// - This initializer is *failable* and will return `nil` if decoding the BlurHash string does not result in a valid image. 23 | /// - Ensure the `blurHash` string is valid and not empty to avoid a `nil` result. 24 | /// 25 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 26 | public extension Image { 27 | /// Initializes an `Image` from a BlurHash string. 28 | /// 29 | /// If the decoding process fails or returns an invalid image, the initializer returns `nil`. 30 | /// 31 | /// - Parameters: 32 | /// - blurHash: The BlurHash-encoded string. 33 | /// - size: The desired output image size. Default is 32x32. 34 | /// - punch: A contrast adjustment factor. Default is 1. 35 | init?(blurHash: String, size: CGSize = CGSize(width: 32, height: 32), punch: Float = 1) { 36 | let unifiedImg = UnifiedImage(blurHash: blurHash, size: size, punch: punch) 37 | guard let unifiedImg = unifiedImg else { 38 | return nil 39 | } 40 | self.init(unifiedImage: unifiedImg) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/UnifiedBlurHash/UnifiedBlurHash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedBlurHash.swift 3 | // 4 | // 5 | // Created by Ian on 12/12/2022. 6 | // 7 | // Many Thanks to Mortenjust for https://github.com/mortenjust/Blurhash-macos.git 8 | 9 | import SwiftUI 10 | 11 | /// A utility struct for working with BlurHash-encoded images and operations related to encoding/decoding BlurHashes. 12 | /// 13 | /// Provides asynchronous methods to: 14 | /// - Generate a BlurHash string from a `UnifiedImage`. 15 | /// - Create a `UnifiedImage` from a BlurHash string. 16 | /// - Decode the average color from a BlurHash string. 17 | /// 18 | public struct UnifiedBlurHash { 19 | 20 | /// Initializes a new instance of `UnifiedBlurHash`. 21 | public init() {} 22 | 23 | /// Generates a BlurHash string from the provided `UnifiedImage`. 24 | /// 25 | /// - Parameter unifiedImage: The `UnifiedImage` instance to encode. 26 | /// - Returns: An optional `String` containing the BlurHash, or `nil` if the image cannot be encoded (e.g., `unifiedImage.small` is nil). 27 | /// 28 | /// - Important: Uses `(4, 3)` as the number of components for BlurHash encoding. 29 | /// 30 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 31 | public static func getBlurHashString(from unifiedImage: UnifiedImage) async -> String? { 32 | unifiedImage.small?.blurHash(numberOfComponents: (4, 3)) 33 | } 34 | 35 | /// Decodes a BlurHash string into a `UnifiedImage`. 36 | /// 37 | /// - Parameter blurHashString: The BlurHash string to decode. 38 | /// - Returns: A `UnifiedImage` created from the given BlurHash, or `nil` if decoding fails. 39 | /// 40 | /// - Note: Uses a fixed output image size of 32x32 pixels. 41 | /// 42 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 43 | public static func getUnifiedImage(from blurHashString: String) async -> UnifiedImage? { 44 | UnifiedImage(blurHash: blurHashString, size: .init(width: 32, height: 32)) 45 | } 46 | 47 | /// Computes the average color of an image represented by a BlurHash string. 48 | /// 49 | /// - Parameter blurHashString: The BlurHash string to analyze. 50 | /// - Returns: A `Color` representing the average color in the decoded image. 51 | /// 52 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 53 | public static func getAverageColor(from blurHashString: String) async -> Color { 54 | decodeAverageColor(blurHash: blurHashString) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/UnifiedBlurHash/UnifiedImage+Decode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedImage+Decode.swift 3 | // 4 | // 5 | // Created by Ian on 12/12/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension UnifiedImage { 11 | public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { 12 | guard blurHash.count >= 6 else { return nil } 13 | 14 | let sizeFlag = String(blurHash[0]).decode83() 15 | let numY = (sizeFlag / 9) + 1 16 | let numX = (sizeFlag % 9) + 1 17 | 18 | let quantisedMaximumValue = String(blurHash[1]).decode83() 19 | let maximumValue = Float(quantisedMaximumValue + 1) / 166 20 | 21 | guard blurHash.count == 4 + 2 * numX * numY else { return nil } 22 | 23 | let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in 24 | if i == 0 { 25 | let value = String(blurHash[2 ..< 6]).decode83() 26 | return decodeDC(value) 27 | } else { 28 | let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() 29 | return decodeAC(value, maximumValue: maximumValue * punch) 30 | } 31 | } 32 | 33 | let width = Int(size.width) 34 | let height = Int(size.height) 35 | let bytesPerRow = width * 3 36 | guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } 37 | CFDataSetLength(data, bytesPerRow * height) 38 | guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } 39 | 40 | var y = 0 41 | 42 | let floatWidth = Float(width) 43 | 44 | let floatHeight = Float(height) 45 | 46 | while y < height { 47 | var x = 0 48 | 49 | let floatY : Float = Float(y) 50 | 51 | while x < width { 52 | var j = 0 53 | 54 | var r: Float = 0 55 | var g: Float = 0 56 | var b: Float = 0 57 | 58 | let floatX : Float = Float(x) 59 | 60 | while j < numY { 61 | var i = 0 62 | 63 | let floatJ : Float = Float(j) 64 | 65 | while i < numX { 66 | let basis = cos(Float.pi * floatX * Float(i) / floatWidth) * cos(Float.pi * floatY * floatJ / floatHeight) 67 | let colour = colours[i + j * numX] 68 | r += colour.0 * basis 69 | g += colour.1 * basis 70 | b += colour.2 * basis 71 | 72 | i += 1 73 | } 74 | 75 | j += 1 76 | } 77 | 78 | let intR = UInt8(Math.linearTosRGB(r)) 79 | let intG = UInt8(Math.linearTosRGB(g)) 80 | let intB = UInt8(Math.linearTosRGB(b)) 81 | 82 | pixels[3 * x + 0 + y * bytesPerRow] = intR 83 | pixels[3 * x + 1 + y * bytesPerRow] = intG 84 | pixels[3 * x + 2 + y * bytesPerRow] = intB 85 | 86 | x += 1 87 | } 88 | 89 | y += 1 90 | } 91 | 92 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) 93 | 94 | guard let provider = CGDataProvider(data: data) else { return nil } 95 | let cgImage = CGImage( 96 | width: width, 97 | height: height, 98 | bitsPerComponent: 8, 99 | bitsPerPixel: 24, 100 | bytesPerRow: bytesPerRow, 101 | space: CGColorSpaceCreateDeviceRGB(), 102 | bitmapInfo: bitmapInfo, 103 | provider: provider, 104 | decode: nil, 105 | shouldInterpolate: true, 106 | intent: .defaultIntent 107 | ) 108 | guard let cgImage = cgImage else { 109 | return nil 110 | } 111 | 112 | #if os (macOS) 113 | self.init(cgImage: cgImage, size: size) 114 | #endif 115 | #if canImport(UIKit) 116 | self.init(cgImage: cgImage) 117 | #endif 118 | } 119 | } 120 | 121 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 122 | func decodeAverageColor(blurHash: String) -> Color { 123 | let value = String(blurHash[2 ..< 6]).decode83() 124 | let intR = value >> 16 125 | let intG = (value >> 8) & 255 126 | let intB = value & 255 127 | return Color(red: Double(intR)/255, green: Double(intG)/255, blue: Double(intB)/255) 128 | } 129 | 130 | private func decodeDC(_ value: Int) -> (Float, Float, Float) { 131 | let intR = value >> 16 132 | let intG = (value >> 8) & 255 133 | let intB = value & 255 134 | return (Math.sRGBToLinear(intR), Math.sRGBToLinear(intG), Math.sRGBToLinear(intB)) 135 | } 136 | 137 | private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { 138 | let quantR = value / (19 * 19) 139 | let quantG = (value / 19) % 19 140 | let quantB = value % 19 141 | 142 | let rgb = ( 143 | Math.signPow((Float(quantR) - 9) / 9, 2) * maximumValue, 144 | Math.signPow((Float(quantG) - 9) / 9, 2) * maximumValue, 145 | Math.signPow((Float(quantB) - 9) / 9, 2) * maximumValue 146 | ) 147 | 148 | return rgb 149 | } 150 | 151 | private let decodeCharacters: [String: Int] = { 152 | var dict: [String: Int] = [:] 153 | for (index, character) in Math.encodeCharacters.map({ String($0) }).enumerated() { 154 | dict[character] = index 155 | } 156 | return dict 157 | }() 158 | 159 | extension String { 160 | func decode83() -> Int { 161 | var value: Int = 0 162 | for character in self { 163 | if let digit = decodeCharacters[String(character)] { 164 | value = value * 83 + digit 165 | } 166 | } 167 | return value 168 | } 169 | } 170 | 171 | private extension String { 172 | subscript (offset: Int) -> Character { 173 | return self[index(startIndex, offsetBy: offset)] 174 | } 175 | 176 | subscript (bounds: CountableClosedRange) -> Substring { 177 | let start = index(startIndex, offsetBy: bounds.lowerBound) 178 | let end = index(startIndex, offsetBy: bounds.upperBound) 179 | return self[start...end] 180 | } 181 | 182 | subscript (bounds: CountableRange) -> Substring { 183 | let start = index(startIndex, offsetBy: bounds.lowerBound) 184 | let end = index(startIndex, offsetBy: bounds.upperBound) 185 | return self[start.. String? { 18 | // Pre-calculate values that are used multiple times 19 | let width = Int(round(size.width)) 20 | let height = Int(round(size.height)) 21 | let size = CGSize(width: width, height: height) 22 | 23 | // Create context with optimized parameters 24 | guard let context = CGContext( 25 | data: nil, 26 | width: width, 27 | height: height, 28 | bitsPerComponent: 8, 29 | bytesPerRow: width * 4, 30 | space: CGColorSpaceCreateDeviceRGB(), 31 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 32 | ) else { 33 | return nil 34 | } 35 | 36 | context.scaleBy(x: 1, y: -1) 37 | context.translateBy(x: 0, y: -size.height) 38 | 39 | #if os(macOS) 40 | let prior = NSGraphicsContext.current 41 | NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true) 42 | draw(in: CGRect(origin: .zero, size: size)) 43 | NSGraphicsContext.current = prior 44 | #elseif os(iOS) 45 | UIGraphicsPushContext(context) 46 | draw(at: .zero) 47 | UIGraphicsPopContext() 48 | #endif 49 | 50 | guard let cgImage = context.makeImage(), 51 | let dataProvider = cgImage.dataProvider, 52 | let data = dataProvider.data, 53 | let pixels = CFDataGetBytePtr(data) else { 54 | return nil 55 | } 56 | 57 | let bytesPerRow = cgImage.bytesPerRow 58 | let bytesPerPixel = cgImage.bitsPerPixel / 8 59 | let pi = Float.pi 60 | 61 | // Pre-calculate component counts 62 | let componentX = components.0 63 | let componentY = components.1 64 | let totalComponents = componentX * componentY 65 | 66 | // Pre-allocate and reserve capacity for arrays 67 | var factors = [(Float, Float, Float)]() 68 | factors.reserveCapacity(totalComponents) 69 | 70 | // Pre-calculate width/height as Float to avoid repeated conversions 71 | let floatWidth = Float(width) 72 | let floatHeight = Float(height) 73 | 74 | // Calculate factors with optimized loops 75 | for y in 0.., 129 | width: Int, 130 | height: Int, 131 | bytesPerRow: Int, 132 | bytesPerPixel: Int, 133 | pixelOffset: Int, 134 | basisFunction: (Float, Float) -> Float 135 | ) -> (Float, Float, Float) { 136 | var r: Float = 0 137 | var g: Float = 0 138 | var b: Float = 0 139 | 140 | let height = height 141 | let width = width 142 | let bytesPerRow = bytesPerRow 143 | let bytesPerPixel = bytesPerPixel 144 | 145 | // Optimized pixel iteration 146 | for y in 0.. Int { 173 | let roundedR = Math.linearTosRGB(value.0) 174 | let roundedG = Math.linearTosRGB(value.1) 175 | let roundedB = Math.linearTosRGB(value.2) 176 | return (roundedR << 16) + (roundedG << 8) + roundedB 177 | } 178 | 179 | @inline(__always) 180 | private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int { 181 | let quantR = Int(max(0, min(18, floor(Math.signPow(value.0 / maximumValue, 0.5) * 9 + 9.5)))) 182 | let quantG = Int(max(0, min(18, floor(Math.signPow(value.1 / maximumValue, 0.5) * 9 + 9.5)))) 183 | let quantB = Int(max(0, min(18, floor(Math.signPow(value.2 / maximumValue, 0.5) * 9 + 9.5)))) 184 | 185 | return quantR * 361 + quantG * 19 + quantB // 19*19 = 361 186 | } 187 | 188 | extension BinaryInteger { 189 | func encode83(length: Int) -> String { 190 | var result = "" 191 | result.reserveCapacity(length) 192 | var value = Int(self) 193 | 194 | for _ in 1...length { 195 | let digit = value % 83 196 | value /= 83 197 | result.append(Math.encodeCharacters[digit]) 198 | } 199 | 200 | return String(result.reversed()) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/UnifiedBlurHash/UnifiedImage+Scale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedImage+Scale.swift 3 | // 4 | // 5 | // Created by Ian on 12/12/2022. 6 | // 7 | import SwiftUI 8 | 9 | /// An extension to `UnifiedImage` that provides a computed property `small` 10 | /// to generate a resized image with a size of 32x32 pixels. 11 | public extension UnifiedImage { 12 | 13 | /// Returns a resized image with a size of 32x32 pixels. 14 | /// 15 | /// - Returns: A resized `UnifiedImage` if resizing is successful, `nil` otherwise. 16 | var small: UnifiedImage? { 17 | resized(to: CGSize(width: 32, height: 32)) 18 | } 19 | } 20 | 21 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 22 | import AppKit 23 | 24 | public extension UnifiedImage { 25 | 26 | /// Resizes the current image to the specified `newSize`. 27 | /// 28 | /// - Parameter newSize: The desired size to resize the image to. 29 | /// - Returns: A new `UnifiedImage` resized to the provided size, or `nil` if resizing fails. 30 | func resized(to newSize: NSSize) -> UnifiedImage? { 31 | let image = NSImage(size: newSize) 32 | image.lockFocus() 33 | let context = NSGraphicsContext.current 34 | context?.imageInterpolation = .high 35 | draw( 36 | in: NSRect(origin: .zero, size: newSize), 37 | from: NSRect(origin: .zero, size: size), 38 | operation: .copy, 39 | fraction: 1 40 | ) 41 | image.unlockFocus() 42 | return image 43 | } 44 | } 45 | 46 | #endif 47 | 48 | #if canImport(UIKit) && !os(watchOS) 49 | import UIKit 50 | import Foundation 51 | 52 | public extension UnifiedImage { 53 | 54 | /// Resizes the current image to the specified `newSize`. 55 | /// 56 | /// - Parameter newSize: The desired size to resize the image to. 57 | /// - Returns: A new `UnifiedImage` resized to the provided size, or `nil` if resizing fails. 58 | func resized(to newSize: CGSize) -> UnifiedImage? { 59 | let renderer = UIGraphicsImageRenderer(size: newSize) 60 | 61 | let image = renderer.image { (context) in 62 | self.draw(in: CGRect(origin: .zero, size: newSize)) 63 | } 64 | return image 65 | } 66 | } 67 | 68 | #elseif os(watchOS) 69 | 70 | import UIKit 71 | import Foundation 72 | 73 | public extension UnifiedImage { 74 | 75 | /// Resizes the current image to the specified `newSize`. 76 | /// 77 | /// - Parameter newSize: The desired size to resize the image to. 78 | /// - Returns: A resized `UnifiedImage` or `nil` if resizing fails. 79 | func resized(to newSize: CGSize) -> UnifiedImage? { 80 | UIGraphicsBeginImageContextWithOptions(size, false, self.scale) 81 | defer { UIGraphicsEndImageContext() } 82 | draw(in: CGRect(origin: .zero, size: newSize)) 83 | return UIGraphicsGetImageFromCurrentImageContext() 84 | } 85 | } 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /Sources/UnifiedBlurHash/UnifiedImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedImage.swift 3 | // 4 | // 5 | // Created by Ian on 12/12/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(UIKit) 11 | import UIKit 12 | 13 | /// A typealias representing a platform-independent image. 14 | /// 15 | /// On iOS, tvOS, and watchOS, this maps to `UIImage`. 16 | public typealias UnifiedImage = UIImage 17 | 18 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 19 | public extension Image { 20 | /// Initializes a SwiftUI `Image` from a `UnifiedImage`. 21 | /// 22 | /// - Parameter unifiedImage: A `UIImage` to be wrapped in a SwiftUI `Image`. 23 | init(unifiedImage: UnifiedImage) { 24 | self.init(uiImage: unifiedImage) 25 | } 26 | } 27 | #endif 28 | 29 | #if os(macOS) 30 | import AppKit 31 | 32 | /// A typealias representing a platform-independent image. 33 | /// 34 | /// On macOS, this maps to `NSImage`. 35 | public typealias UnifiedImage = NSImage 36 | 37 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 38 | public extension Image { 39 | /// Initializes a SwiftUI `Image` from a `UnifiedImage`. 40 | /// 41 | /// - Parameter unifiedImage: An `NSImage` to be wrapped in a SwiftUI `Image`. 42 | init(unifiedImage: UnifiedImage) { 43 | self.init(nsImage: unifiedImage) 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Tests/UnifiedBlurHashTests/UnifiedBlurHashTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedBlurHashTests.swift 3 | // UnifiedBlurHash 4 | // 5 | // Created by ian on 3/9/25. 6 | // 7 | 8 | import Testing 9 | @testable import UnifiedBlurHash 10 | import Foundation 11 | import SwiftUI 12 | 13 | @Suite("UnifiedBlurHash Functionality Tests") 14 | class UnifiedBlurHashTests { 15 | 16 | @Test("Decode valid blur hash string into UnifiedImage") 17 | func testDecodeValidBlurHash() async { 18 | let blurHash = "LWC?S~x]Sjof~Wx]X9oe?btRofax" // Valid placeholder BlurHash 19 | let image = await UnifiedBlurHash.getUnifiedImage(from: blurHash) 20 | 21 | #expect(image != nil) 22 | #expect(image?.size.width == 32) 23 | #expect(image?.size.height == 32) 24 | } 25 | 26 | @Test("Get blur hash string from a valid UnifiedImage") 27 | func testEncodeUnifiedImageToBlurHash() async { 28 | guard let image = Bundle.module.image(forResource: "sunflower") else { 29 | #expect(Bool(false), "Test image not found in bundle") 30 | return 31 | } 32 | let hash = await UnifiedBlurHash.getBlurHashString(from: image) 33 | #expect(hash == "LWC?S~x]Sjof~Wx]X9oe?btRofax") 34 | #expect(hash != nil && hash!.count >= 6) 35 | } 36 | 37 | @Test("Get average color from valid BlurHash string") 38 | func testAverageColorFromBlurHash() async { 39 | let blurHash = "LWC?S~x]Sjof~Wx]X9oe?btRofax" 40 | let color = await UnifiedBlurHash.getAverageColor(from: blurHash) 41 | 42 | // You can’t compare Color directly, but we can at least ensure it's not black or clear. 43 | let components = color.description.lowercased() 44 | #expect(!components.contains("clear")) 45 | } 46 | 47 | @Test("Fail to decode invalid blur hash string") 48 | func testInvalidBlurHashDecoding() async { 49 | let invalidHash = "LWC" // Too short to be valid 50 | let image = await UnifiedBlurHash.getUnifiedImage(from: invalidHash) 51 | #expect(image == nil) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/UnifiedBlurHashTests/UnifiedImageResizingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedImageResizingTests.swift 3 | // UnifiedBlurHash 4 | // 5 | // Created by ian on 03/05/2025. 6 | // 7 | 8 | 9 | import Testing 10 | @testable import UnifiedBlurHash 11 | import Foundation 12 | import SwiftUI 13 | 14 | @Suite("UnifiedImage Resizing Tests") 15 | class UnifiedImageResizingTests { 16 | 17 | @Test("Resizing an image to 32x32 produces the expected size") 18 | func testResizingTo32x32() { 19 | // Create an example image with a known size 20 | guard let originalImage = Bundle.module.image(forResource: "sunflower") else { 21 | #expect(Bool(false), "Test image not found in bundle") 22 | return 23 | } 24 | 25 | // Resize the image to 32x32 26 | let resizedImage = originalImage.resized(to: CGSize(width: 32, height: 32)) 27 | 28 | #expect(resizedImage?.size.width == 32) 29 | #expect(resizedImage?.size.height == 32) 30 | } 31 | 32 | @Test("Resizing returns nil for invalid image") 33 | func testResizingInvalidImage() { 34 | let invalidImage: UnifiedImage? = nil 35 | 36 | let resizedImage = invalidImage?.resized(to: CGSize(width: 32, height: 32)) 37 | 38 | #expect(resizedImage == nil) 39 | } 40 | 41 | @Test("Resizing an image produces a non-nil result") 42 | func testResizingValidImage() { 43 | // Create a valid image from a system image 44 | guard let originalImage = Bundle.module.image(forResource: "sunflower") else { 45 | #expect(Bool(false), "Test image not found in bundle") 46 | return 47 | } 48 | 49 | // Resize the image 50 | let resizedImage = originalImage.resized(to: CGSize(width: 64, height: 64)) 51 | 52 | #expect(resizedImage != nil) 53 | #expect(resizedImage?.size.width == 64) 54 | #expect(resizedImage?.size.height == 64) 55 | } 56 | 57 | @Test("Small property returns 32x32 resized image") 58 | func testSmallProperty() { 59 | // Create an example image with a known size 60 | guard let originalImage = Bundle.module.image(forResource: "sunflower") else { 61 | #expect(Bool(false), "Test image not found in bundle") 62 | return 63 | } 64 | 65 | // Use the `small` property to resize the image 66 | let smallImage = originalImage.small 67 | 68 | #expect(smallImage?.size.width == 32) 69 | #expect(smallImage?.size.height == 32) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/UnifiedBlurHashTests/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestUtilities.swift 3 | // UnifiedBlurHash 4 | // 5 | // Created by ian on 03/05/2025. 6 | // 7 | 8 | import Foundation 9 | import UnifiedBlurHash 10 | 11 | #if canImport(UIKit) 12 | import UIKit 13 | 14 | extension Bundle { 15 | func image(forResource name: String) -> UnifiedImage? { 16 | // guard let path = self.path(forResource: name, ofType: nil) else { 17 | // return nil 18 | // } 19 | return UIImage(named: name, in: self, compatibleWith: nil) 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /demo/demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3C140DBF2A51952F00A8783E /* demoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140DBE2A51952F00A8783E /* demoApp.swift */; }; 11 | 3C140DC12A51952F00A8783E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140DC02A51952F00A8783E /* ContentView.swift */; }; 12 | 3C140DC32A51953100A8783E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3C140DC22A51953100A8783E /* Assets.xcassets */; }; 13 | 3C140DC72A51953100A8783E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3C140DC62A51953100A8783E /* Preview Assets.xcassets */; }; 14 | 3C140DD12A51953200A8783E /* demoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140DD02A51953200A8783E /* demoTests.swift */; }; 15 | 3C140DDB2A51953200A8783E /* demoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140DDA2A51953200A8783E /* demoUITests.swift */; }; 16 | 3C140DDD2A51953200A8783E /* demoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140DDC2A51953200A8783E /* demoUITestsLaunchTests.swift */; }; 17 | 3C140DED2A51960200A8783E /* UnifiedBlurHash in Frameworks */ = {isa = PBXBuildFile; productRef = 3C140DEC2A51960200A8783E /* UnifiedBlurHash */; }; 18 | 3C140E2D2A51C81600A8783E /* watchOS_demoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140E2C2A51C81600A8783E /* watchOS_demoApp.swift */; }; 19 | 3C140E2F2A51C81600A8783E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140E2E2A51C81600A8783E /* ContentView.swift */; }; 20 | 3C140E312A51C81900A8783E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3C140E302A51C81900A8783E /* Assets.xcassets */; }; 21 | 3C140E342A51C81900A8783E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3C140E332A51C81900A8783E /* Preview Assets.xcassets */; }; 22 | 3C140E3E2A51C81A00A8783E /* watchOS_demo_Watch_AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140E3D2A51C81A00A8783E /* watchOS_demo_Watch_AppTests.swift */; }; 23 | 3C140E482A51C81A00A8783E /* watchOS_demo_Watch_AppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140E472A51C81A00A8783E /* watchOS_demo_Watch_AppUITests.swift */; }; 24 | 3C140E4A2A51C81A00A8783E /* watchOS_demo_Watch_AppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C140E492A51C81A00A8783E /* watchOS_demo_Watch_AppUITestsLaunchTests.swift */; }; 25 | 3C140E4D2A51C81A00A8783E /* watchOS-demo Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 3C140E2A2A51C81600A8783E /* watchOS-demo Watch App.app */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 26 | 3C140E5B2A51C8CC00A8783E /* UnifiedBlurHash in Frameworks */ = {isa = PBXBuildFile; productRef = 3C140E5A2A51C8CC00A8783E /* UnifiedBlurHash */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | 3C140DCD2A51953200A8783E /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 3C140DB32A51952F00A8783E /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 3C140DBA2A51952F00A8783E; 35 | remoteInfo = demo; 36 | }; 37 | 3C140DD72A51953200A8783E /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 3C140DB32A51952F00A8783E /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = 3C140DBA2A51952F00A8783E; 42 | remoteInfo = demo; 43 | }; 44 | 3C140E3A2A51C81900A8783E /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = 3C140DB32A51952F00A8783E /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = 3C140E292A51C81600A8783E; 49 | remoteInfo = "watchOS-demo Watch App"; 50 | }; 51 | 3C140E442A51C81A00A8783E /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = 3C140DB32A51952F00A8783E /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = 3C140E292A51C81600A8783E; 56 | remoteInfo = "watchOS-demo Watch App"; 57 | }; 58 | 3C140E4B2A51C81A00A8783E /* PBXContainerItemProxy */ = { 59 | isa = PBXContainerItemProxy; 60 | containerPortal = 3C140DB32A51952F00A8783E /* Project object */; 61 | proxyType = 1; 62 | remoteGlobalIDString = 3C140E292A51C81600A8783E; 63 | remoteInfo = "watchOS-demo Watch App"; 64 | }; 65 | /* End PBXContainerItemProxy section */ 66 | 67 | /* Begin PBXCopyFilesBuildPhase section */ 68 | 3C140E512A51C81B00A8783E /* Embed Watch Content */ = { 69 | isa = PBXCopyFilesBuildPhase; 70 | buildActionMask = 2147483647; 71 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; 72 | dstSubfolderSpec = 16; 73 | files = ( 74 | 3C140E4D2A51C81A00A8783E /* watchOS-demo Watch App.app in Embed Watch Content */, 75 | ); 76 | name = "Embed Watch Content"; 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXCopyFilesBuildPhase section */ 80 | 81 | /* Begin PBXFileReference section */ 82 | 3C140DBB2A51952F00A8783E /* demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | 3C140DBE2A51952F00A8783E /* demoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demoApp.swift; sourceTree = ""; }; 84 | 3C140DC02A51952F00A8783E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 85 | 3C140DC22A51953100A8783E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 86 | 3C140DC42A51953100A8783E /* demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = demo.entitlements; sourceTree = ""; }; 87 | 3C140DC62A51953100A8783E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 88 | 3C140DCC2A51953200A8783E /* demoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = demoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | 3C140DD02A51953200A8783E /* demoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demoTests.swift; sourceTree = ""; }; 90 | 3C140DD62A51953200A8783E /* demoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = demoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 91 | 3C140DDA2A51953200A8783E /* demoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demoUITests.swift; sourceTree = ""; }; 92 | 3C140DDC2A51953200A8783E /* demoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demoUITestsLaunchTests.swift; sourceTree = ""; }; 93 | 3C140DEA2A5195DF00A8783E /* UnifiedBlurHash */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = UnifiedBlurHash; path = ..; sourceTree = ""; }; 94 | 3C140E2A2A51C81600A8783E /* watchOS-demo Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "watchOS-demo Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 95 | 3C140E2C2A51C81600A8783E /* watchOS_demoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = watchOS_demoApp.swift; sourceTree = ""; }; 96 | 3C140E2E2A51C81600A8783E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 97 | 3C140E302A51C81900A8783E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 98 | 3C140E332A51C81900A8783E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 99 | 3C140E392A51C81900A8783E /* watchOS-demo Watch AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "watchOS-demo Watch AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 100 | 3C140E3D2A51C81A00A8783E /* watchOS_demo_Watch_AppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = watchOS_demo_Watch_AppTests.swift; sourceTree = ""; }; 101 | 3C140E432A51C81A00A8783E /* watchOS-demo Watch AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "watchOS-demo Watch AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 102 | 3C140E472A51C81A00A8783E /* watchOS_demo_Watch_AppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = watchOS_demo_Watch_AppUITests.swift; sourceTree = ""; }; 103 | 3C140E492A51C81A00A8783E /* watchOS_demo_Watch_AppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = watchOS_demo_Watch_AppUITestsLaunchTests.swift; sourceTree = ""; }; 104 | /* End PBXFileReference section */ 105 | 106 | /* Begin PBXFrameworksBuildPhase section */ 107 | 3C140DB82A51952F00A8783E /* Frameworks */ = { 108 | isa = PBXFrameworksBuildPhase; 109 | buildActionMask = 2147483647; 110 | files = ( 111 | 3C140DED2A51960200A8783E /* UnifiedBlurHash in Frameworks */, 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | 3C140DC92A51953200A8783E /* Frameworks */ = { 116 | isa = PBXFrameworksBuildPhase; 117 | buildActionMask = 2147483647; 118 | files = ( 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | 3C140DD32A51953200A8783E /* Frameworks */ = { 123 | isa = PBXFrameworksBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | 3C140E272A51C81600A8783E /* Frameworks */ = { 130 | isa = PBXFrameworksBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | 3C140E5B2A51C8CC00A8783E /* UnifiedBlurHash in Frameworks */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | 3C140E362A51C81900A8783E /* Frameworks */ = { 138 | isa = PBXFrameworksBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | 3C140E402A51C81A00A8783E /* Frameworks */ = { 145 | isa = PBXFrameworksBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXFrameworksBuildPhase section */ 152 | 153 | /* Begin PBXGroup section */ 154 | 3C140DB22A51952F00A8783E = { 155 | isa = PBXGroup; 156 | children = ( 157 | 3C140DE92A5195DF00A8783E /* Packages */, 158 | 3C140DBD2A51952F00A8783E /* demo */, 159 | 3C140DCF2A51953200A8783E /* demoTests */, 160 | 3C140DD92A51953200A8783E /* demoUITests */, 161 | 3C140E2B2A51C81600A8783E /* watchOS-demo Watch App */, 162 | 3C140E3C2A51C81A00A8783E /* watchOS-demo Watch AppTests */, 163 | 3C140E462A51C81A00A8783E /* watchOS-demo Watch AppUITests */, 164 | 3C140DBC2A51952F00A8783E /* Products */, 165 | 3C140DEB2A51960200A8783E /* Frameworks */, 166 | ); 167 | sourceTree = ""; 168 | }; 169 | 3C140DBC2A51952F00A8783E /* Products */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 3C140DBB2A51952F00A8783E /* demo.app */, 173 | 3C140DCC2A51953200A8783E /* demoTests.xctest */, 174 | 3C140DD62A51953200A8783E /* demoUITests.xctest */, 175 | 3C140E2A2A51C81600A8783E /* watchOS-demo Watch App.app */, 176 | 3C140E392A51C81900A8783E /* watchOS-demo Watch AppTests.xctest */, 177 | 3C140E432A51C81A00A8783E /* watchOS-demo Watch AppUITests.xctest */, 178 | ); 179 | name = Products; 180 | sourceTree = ""; 181 | }; 182 | 3C140DBD2A51952F00A8783E /* demo */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 3C140DBE2A51952F00A8783E /* demoApp.swift */, 186 | 3C140DC02A51952F00A8783E /* ContentView.swift */, 187 | 3C140DC22A51953100A8783E /* Assets.xcassets */, 188 | 3C140DC42A51953100A8783E /* demo.entitlements */, 189 | 3C140DC52A51953100A8783E /* Preview Content */, 190 | ); 191 | path = demo; 192 | sourceTree = ""; 193 | }; 194 | 3C140DC52A51953100A8783E /* Preview Content */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | 3C140DC62A51953100A8783E /* Preview Assets.xcassets */, 198 | ); 199 | path = "Preview Content"; 200 | sourceTree = ""; 201 | }; 202 | 3C140DCF2A51953200A8783E /* demoTests */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 3C140DD02A51953200A8783E /* demoTests.swift */, 206 | ); 207 | path = demoTests; 208 | sourceTree = ""; 209 | }; 210 | 3C140DD92A51953200A8783E /* demoUITests */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 3C140DDA2A51953200A8783E /* demoUITests.swift */, 214 | 3C140DDC2A51953200A8783E /* demoUITestsLaunchTests.swift */, 215 | ); 216 | path = demoUITests; 217 | sourceTree = ""; 218 | }; 219 | 3C140DE92A5195DF00A8783E /* Packages */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 3C140DEA2A5195DF00A8783E /* UnifiedBlurHash */, 223 | ); 224 | name = Packages; 225 | sourceTree = ""; 226 | }; 227 | 3C140DEB2A51960200A8783E /* Frameworks */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | ); 231 | name = Frameworks; 232 | sourceTree = ""; 233 | }; 234 | 3C140E2B2A51C81600A8783E /* watchOS-demo Watch App */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 3C140E2C2A51C81600A8783E /* watchOS_demoApp.swift */, 238 | 3C140E2E2A51C81600A8783E /* ContentView.swift */, 239 | 3C140E302A51C81900A8783E /* Assets.xcassets */, 240 | 3C140E322A51C81900A8783E /* Preview Content */, 241 | ); 242 | path = "watchOS-demo Watch App"; 243 | sourceTree = ""; 244 | }; 245 | 3C140E322A51C81900A8783E /* Preview Content */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | 3C140E332A51C81900A8783E /* Preview Assets.xcassets */, 249 | ); 250 | path = "Preview Content"; 251 | sourceTree = ""; 252 | }; 253 | 3C140E3C2A51C81A00A8783E /* watchOS-demo Watch AppTests */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | 3C140E3D2A51C81A00A8783E /* watchOS_demo_Watch_AppTests.swift */, 257 | ); 258 | path = "watchOS-demo Watch AppTests"; 259 | sourceTree = ""; 260 | }; 261 | 3C140E462A51C81A00A8783E /* watchOS-demo Watch AppUITests */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | 3C140E472A51C81A00A8783E /* watchOS_demo_Watch_AppUITests.swift */, 265 | 3C140E492A51C81A00A8783E /* watchOS_demo_Watch_AppUITestsLaunchTests.swift */, 266 | ); 267 | path = "watchOS-demo Watch AppUITests"; 268 | sourceTree = ""; 269 | }; 270 | /* End PBXGroup section */ 271 | 272 | /* Begin PBXNativeTarget section */ 273 | 3C140DBA2A51952F00A8783E /* demo */ = { 274 | isa = PBXNativeTarget; 275 | buildConfigurationList = 3C140DE02A51953200A8783E /* Build configuration list for PBXNativeTarget "demo" */; 276 | buildPhases = ( 277 | 3C140DB72A51952F00A8783E /* Sources */, 278 | 3C140DB82A51952F00A8783E /* Frameworks */, 279 | 3C140DB92A51952F00A8783E /* Resources */, 280 | 3C140E512A51C81B00A8783E /* Embed Watch Content */, 281 | ); 282 | buildRules = ( 283 | ); 284 | dependencies = ( 285 | 3C140E4C2A51C81A00A8783E /* PBXTargetDependency */, 286 | ); 287 | name = demo; 288 | packageProductDependencies = ( 289 | 3C140DEC2A51960200A8783E /* UnifiedBlurHash */, 290 | ); 291 | productName = demo; 292 | productReference = 3C140DBB2A51952F00A8783E /* demo.app */; 293 | productType = "com.apple.product-type.application"; 294 | }; 295 | 3C140DCB2A51953200A8783E /* demoTests */ = { 296 | isa = PBXNativeTarget; 297 | buildConfigurationList = 3C140DE32A51953200A8783E /* Build configuration list for PBXNativeTarget "demoTests" */; 298 | buildPhases = ( 299 | 3C140DC82A51953200A8783E /* Sources */, 300 | 3C140DC92A51953200A8783E /* Frameworks */, 301 | 3C140DCA2A51953200A8783E /* Resources */, 302 | ); 303 | buildRules = ( 304 | ); 305 | dependencies = ( 306 | 3C140DCE2A51953200A8783E /* PBXTargetDependency */, 307 | ); 308 | name = demoTests; 309 | productName = demoTests; 310 | productReference = 3C140DCC2A51953200A8783E /* demoTests.xctest */; 311 | productType = "com.apple.product-type.bundle.unit-test"; 312 | }; 313 | 3C140DD52A51953200A8783E /* demoUITests */ = { 314 | isa = PBXNativeTarget; 315 | buildConfigurationList = 3C140DE62A51953200A8783E /* Build configuration list for PBXNativeTarget "demoUITests" */; 316 | buildPhases = ( 317 | 3C140DD22A51953200A8783E /* Sources */, 318 | 3C140DD32A51953200A8783E /* Frameworks */, 319 | 3C140DD42A51953200A8783E /* Resources */, 320 | ); 321 | buildRules = ( 322 | ); 323 | dependencies = ( 324 | 3C140DD82A51953200A8783E /* PBXTargetDependency */, 325 | ); 326 | name = demoUITests; 327 | productName = demoUITests; 328 | productReference = 3C140DD62A51953200A8783E /* demoUITests.xctest */; 329 | productType = "com.apple.product-type.bundle.ui-testing"; 330 | }; 331 | 3C140E292A51C81600A8783E /* watchOS-demo Watch App */ = { 332 | isa = PBXNativeTarget; 333 | buildConfigurationList = 3C140E4E2A51C81B00A8783E /* Build configuration list for PBXNativeTarget "watchOS-demo Watch App" */; 334 | buildPhases = ( 335 | 3C140E262A51C81600A8783E /* Sources */, 336 | 3C140E272A51C81600A8783E /* Frameworks */, 337 | 3C140E282A51C81600A8783E /* Resources */, 338 | ); 339 | buildRules = ( 340 | ); 341 | dependencies = ( 342 | ); 343 | name = "watchOS-demo Watch App"; 344 | packageProductDependencies = ( 345 | 3C140E5A2A51C8CC00A8783E /* UnifiedBlurHash */, 346 | ); 347 | productName = "watchOS-demo Watch App"; 348 | productReference = 3C140E2A2A51C81600A8783E /* watchOS-demo Watch App.app */; 349 | productType = "com.apple.product-type.application"; 350 | }; 351 | 3C140E382A51C81900A8783E /* watchOS-demo Watch AppTests */ = { 352 | isa = PBXNativeTarget; 353 | buildConfigurationList = 3C140E522A51C81B00A8783E /* Build configuration list for PBXNativeTarget "watchOS-demo Watch AppTests" */; 354 | buildPhases = ( 355 | 3C140E352A51C81900A8783E /* Sources */, 356 | 3C140E362A51C81900A8783E /* Frameworks */, 357 | 3C140E372A51C81900A8783E /* Resources */, 358 | ); 359 | buildRules = ( 360 | ); 361 | dependencies = ( 362 | 3C140E3B2A51C81900A8783E /* PBXTargetDependency */, 363 | ); 364 | name = "watchOS-demo Watch AppTests"; 365 | productName = "watchOS-demo Watch AppTests"; 366 | productReference = 3C140E392A51C81900A8783E /* watchOS-demo Watch AppTests.xctest */; 367 | productType = "com.apple.product-type.bundle.unit-test"; 368 | }; 369 | 3C140E422A51C81A00A8783E /* watchOS-demo Watch AppUITests */ = { 370 | isa = PBXNativeTarget; 371 | buildConfigurationList = 3C140E552A51C81B00A8783E /* Build configuration list for PBXNativeTarget "watchOS-demo Watch AppUITests" */; 372 | buildPhases = ( 373 | 3C140E3F2A51C81A00A8783E /* Sources */, 374 | 3C140E402A51C81A00A8783E /* Frameworks */, 375 | 3C140E412A51C81A00A8783E /* Resources */, 376 | ); 377 | buildRules = ( 378 | ); 379 | dependencies = ( 380 | 3C140E452A51C81A00A8783E /* PBXTargetDependency */, 381 | ); 382 | name = "watchOS-demo Watch AppUITests"; 383 | productName = "watchOS-demo Watch AppUITests"; 384 | productReference = 3C140E432A51C81A00A8783E /* watchOS-demo Watch AppUITests.xctest */; 385 | productType = "com.apple.product-type.bundle.ui-testing"; 386 | }; 387 | /* End PBXNativeTarget section */ 388 | 389 | /* Begin PBXProject section */ 390 | 3C140DB32A51952F00A8783E /* Project object */ = { 391 | isa = PBXProject; 392 | attributes = { 393 | BuildIndependentTargetsInParallel = 1; 394 | LastSwiftUpdateCheck = 1430; 395 | LastUpgradeCheck = 1430; 396 | TargetAttributes = { 397 | 3C140DBA2A51952F00A8783E = { 398 | CreatedOnToolsVersion = 14.3; 399 | }; 400 | 3C140DCB2A51953200A8783E = { 401 | CreatedOnToolsVersion = 14.3; 402 | TestTargetID = 3C140DBA2A51952F00A8783E; 403 | }; 404 | 3C140DD52A51953200A8783E = { 405 | CreatedOnToolsVersion = 14.3; 406 | TestTargetID = 3C140DBA2A51952F00A8783E; 407 | }; 408 | 3C140E292A51C81600A8783E = { 409 | CreatedOnToolsVersion = 14.3; 410 | }; 411 | 3C140E382A51C81900A8783E = { 412 | CreatedOnToolsVersion = 14.3; 413 | TestTargetID = 3C140E292A51C81600A8783E; 414 | }; 415 | 3C140E422A51C81A00A8783E = { 416 | CreatedOnToolsVersion = 14.3; 417 | TestTargetID = 3C140E292A51C81600A8783E; 418 | }; 419 | }; 420 | }; 421 | buildConfigurationList = 3C140DB62A51952F00A8783E /* Build configuration list for PBXProject "demo" */; 422 | compatibilityVersion = "Xcode 14.0"; 423 | developmentRegion = en; 424 | hasScannedForEncodings = 0; 425 | knownRegions = ( 426 | en, 427 | Base, 428 | ); 429 | mainGroup = 3C140DB22A51952F00A8783E; 430 | productRefGroup = 3C140DBC2A51952F00A8783E /* Products */; 431 | projectDirPath = ""; 432 | projectRoot = ""; 433 | targets = ( 434 | 3C140DBA2A51952F00A8783E /* demo */, 435 | 3C140DCB2A51953200A8783E /* demoTests */, 436 | 3C140DD52A51953200A8783E /* demoUITests */, 437 | 3C140E292A51C81600A8783E /* watchOS-demo Watch App */, 438 | 3C140E382A51C81900A8783E /* watchOS-demo Watch AppTests */, 439 | 3C140E422A51C81A00A8783E /* watchOS-demo Watch AppUITests */, 440 | ); 441 | }; 442 | /* End PBXProject section */ 443 | 444 | /* Begin PBXResourcesBuildPhase section */ 445 | 3C140DB92A51952F00A8783E /* Resources */ = { 446 | isa = PBXResourcesBuildPhase; 447 | buildActionMask = 2147483647; 448 | files = ( 449 | 3C140DC72A51953100A8783E /* Preview Assets.xcassets in Resources */, 450 | 3C140DC32A51953100A8783E /* Assets.xcassets in Resources */, 451 | ); 452 | runOnlyForDeploymentPostprocessing = 0; 453 | }; 454 | 3C140DCA2A51953200A8783E /* Resources */ = { 455 | isa = PBXResourcesBuildPhase; 456 | buildActionMask = 2147483647; 457 | files = ( 458 | ); 459 | runOnlyForDeploymentPostprocessing = 0; 460 | }; 461 | 3C140DD42A51953200A8783E /* Resources */ = { 462 | isa = PBXResourcesBuildPhase; 463 | buildActionMask = 2147483647; 464 | files = ( 465 | ); 466 | runOnlyForDeploymentPostprocessing = 0; 467 | }; 468 | 3C140E282A51C81600A8783E /* Resources */ = { 469 | isa = PBXResourcesBuildPhase; 470 | buildActionMask = 2147483647; 471 | files = ( 472 | 3C140E342A51C81900A8783E /* Preview Assets.xcassets in Resources */, 473 | 3C140E312A51C81900A8783E /* Assets.xcassets in Resources */, 474 | ); 475 | runOnlyForDeploymentPostprocessing = 0; 476 | }; 477 | 3C140E372A51C81900A8783E /* Resources */ = { 478 | isa = PBXResourcesBuildPhase; 479 | buildActionMask = 2147483647; 480 | files = ( 481 | ); 482 | runOnlyForDeploymentPostprocessing = 0; 483 | }; 484 | 3C140E412A51C81A00A8783E /* Resources */ = { 485 | isa = PBXResourcesBuildPhase; 486 | buildActionMask = 2147483647; 487 | files = ( 488 | ); 489 | runOnlyForDeploymentPostprocessing = 0; 490 | }; 491 | /* End PBXResourcesBuildPhase section */ 492 | 493 | /* Begin PBXSourcesBuildPhase section */ 494 | 3C140DB72A51952F00A8783E /* Sources */ = { 495 | isa = PBXSourcesBuildPhase; 496 | buildActionMask = 2147483647; 497 | files = ( 498 | 3C140DC12A51952F00A8783E /* ContentView.swift in Sources */, 499 | 3C140DBF2A51952F00A8783E /* demoApp.swift in Sources */, 500 | ); 501 | runOnlyForDeploymentPostprocessing = 0; 502 | }; 503 | 3C140DC82A51953200A8783E /* Sources */ = { 504 | isa = PBXSourcesBuildPhase; 505 | buildActionMask = 2147483647; 506 | files = ( 507 | 3C140DD12A51953200A8783E /* demoTests.swift in Sources */, 508 | ); 509 | runOnlyForDeploymentPostprocessing = 0; 510 | }; 511 | 3C140DD22A51953200A8783E /* Sources */ = { 512 | isa = PBXSourcesBuildPhase; 513 | buildActionMask = 2147483647; 514 | files = ( 515 | 3C140DDB2A51953200A8783E /* demoUITests.swift in Sources */, 516 | 3C140DDD2A51953200A8783E /* demoUITestsLaunchTests.swift in Sources */, 517 | ); 518 | runOnlyForDeploymentPostprocessing = 0; 519 | }; 520 | 3C140E262A51C81600A8783E /* Sources */ = { 521 | isa = PBXSourcesBuildPhase; 522 | buildActionMask = 2147483647; 523 | files = ( 524 | 3C140E2F2A51C81600A8783E /* ContentView.swift in Sources */, 525 | 3C140E2D2A51C81600A8783E /* watchOS_demoApp.swift in Sources */, 526 | ); 527 | runOnlyForDeploymentPostprocessing = 0; 528 | }; 529 | 3C140E352A51C81900A8783E /* Sources */ = { 530 | isa = PBXSourcesBuildPhase; 531 | buildActionMask = 2147483647; 532 | files = ( 533 | 3C140E3E2A51C81A00A8783E /* watchOS_demo_Watch_AppTests.swift in Sources */, 534 | ); 535 | runOnlyForDeploymentPostprocessing = 0; 536 | }; 537 | 3C140E3F2A51C81A00A8783E /* Sources */ = { 538 | isa = PBXSourcesBuildPhase; 539 | buildActionMask = 2147483647; 540 | files = ( 541 | 3C140E482A51C81A00A8783E /* watchOS_demo_Watch_AppUITests.swift in Sources */, 542 | 3C140E4A2A51C81A00A8783E /* watchOS_demo_Watch_AppUITestsLaunchTests.swift in Sources */, 543 | ); 544 | runOnlyForDeploymentPostprocessing = 0; 545 | }; 546 | /* End PBXSourcesBuildPhase section */ 547 | 548 | /* Begin PBXTargetDependency section */ 549 | 3C140DCE2A51953200A8783E /* PBXTargetDependency */ = { 550 | isa = PBXTargetDependency; 551 | target = 3C140DBA2A51952F00A8783E /* demo */; 552 | targetProxy = 3C140DCD2A51953200A8783E /* PBXContainerItemProxy */; 553 | }; 554 | 3C140DD82A51953200A8783E /* PBXTargetDependency */ = { 555 | isa = PBXTargetDependency; 556 | target = 3C140DBA2A51952F00A8783E /* demo */; 557 | targetProxy = 3C140DD72A51953200A8783E /* PBXContainerItemProxy */; 558 | }; 559 | 3C140E3B2A51C81900A8783E /* PBXTargetDependency */ = { 560 | isa = PBXTargetDependency; 561 | target = 3C140E292A51C81600A8783E /* watchOS-demo Watch App */; 562 | targetProxy = 3C140E3A2A51C81900A8783E /* PBXContainerItemProxy */; 563 | }; 564 | 3C140E452A51C81A00A8783E /* PBXTargetDependency */ = { 565 | isa = PBXTargetDependency; 566 | target = 3C140E292A51C81600A8783E /* watchOS-demo Watch App */; 567 | targetProxy = 3C140E442A51C81A00A8783E /* PBXContainerItemProxy */; 568 | }; 569 | 3C140E4C2A51C81A00A8783E /* PBXTargetDependency */ = { 570 | isa = PBXTargetDependency; 571 | platformFilter = ios; 572 | target = 3C140E292A51C81600A8783E /* watchOS-demo Watch App */; 573 | targetProxy = 3C140E4B2A51C81A00A8783E /* PBXContainerItemProxy */; 574 | }; 575 | /* End PBXTargetDependency section */ 576 | 577 | /* Begin XCBuildConfiguration section */ 578 | 3C140DDE2A51953200A8783E /* Debug */ = { 579 | isa = XCBuildConfiguration; 580 | buildSettings = { 581 | ALWAYS_SEARCH_USER_PATHS = NO; 582 | CLANG_ANALYZER_NONNULL = YES; 583 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 584 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 585 | CLANG_ENABLE_MODULES = YES; 586 | CLANG_ENABLE_OBJC_ARC = YES; 587 | CLANG_ENABLE_OBJC_WEAK = YES; 588 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 589 | CLANG_WARN_BOOL_CONVERSION = YES; 590 | CLANG_WARN_COMMA = YES; 591 | CLANG_WARN_CONSTANT_CONVERSION = YES; 592 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 593 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 594 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 595 | CLANG_WARN_EMPTY_BODY = YES; 596 | CLANG_WARN_ENUM_CONVERSION = YES; 597 | CLANG_WARN_INFINITE_RECURSION = YES; 598 | CLANG_WARN_INT_CONVERSION = YES; 599 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 600 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 601 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 602 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 603 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 604 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 605 | CLANG_WARN_STRICT_PROTOTYPES = YES; 606 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 607 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 608 | CLANG_WARN_UNREACHABLE_CODE = YES; 609 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 610 | COPY_PHASE_STRIP = NO; 611 | DEBUG_INFORMATION_FORMAT = dwarf; 612 | ENABLE_STRICT_OBJC_MSGSEND = YES; 613 | ENABLE_TESTABILITY = YES; 614 | GCC_C_LANGUAGE_STANDARD = gnu11; 615 | GCC_DYNAMIC_NO_PIC = NO; 616 | GCC_NO_COMMON_BLOCKS = YES; 617 | GCC_OPTIMIZATION_LEVEL = 0; 618 | GCC_PREPROCESSOR_DEFINITIONS = ( 619 | "DEBUG=1", 620 | "$(inherited)", 621 | ); 622 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 623 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 624 | GCC_WARN_UNDECLARED_SELECTOR = YES; 625 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 626 | GCC_WARN_UNUSED_FUNCTION = YES; 627 | GCC_WARN_UNUSED_VARIABLE = YES; 628 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 629 | MTL_FAST_MATH = YES; 630 | ONLY_ACTIVE_ARCH = YES; 631 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 632 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 633 | }; 634 | name = Debug; 635 | }; 636 | 3C140DDF2A51953200A8783E /* Release */ = { 637 | isa = XCBuildConfiguration; 638 | buildSettings = { 639 | ALWAYS_SEARCH_USER_PATHS = NO; 640 | CLANG_ANALYZER_NONNULL = YES; 641 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 642 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 643 | CLANG_ENABLE_MODULES = YES; 644 | CLANG_ENABLE_OBJC_ARC = YES; 645 | CLANG_ENABLE_OBJC_WEAK = YES; 646 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 647 | CLANG_WARN_BOOL_CONVERSION = YES; 648 | CLANG_WARN_COMMA = YES; 649 | CLANG_WARN_CONSTANT_CONVERSION = YES; 650 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 651 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 652 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 653 | CLANG_WARN_EMPTY_BODY = YES; 654 | CLANG_WARN_ENUM_CONVERSION = YES; 655 | CLANG_WARN_INFINITE_RECURSION = YES; 656 | CLANG_WARN_INT_CONVERSION = YES; 657 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 658 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 659 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 660 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 661 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 662 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 663 | CLANG_WARN_STRICT_PROTOTYPES = YES; 664 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 665 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 666 | CLANG_WARN_UNREACHABLE_CODE = YES; 667 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 668 | COPY_PHASE_STRIP = NO; 669 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 670 | ENABLE_NS_ASSERTIONS = NO; 671 | ENABLE_STRICT_OBJC_MSGSEND = YES; 672 | GCC_C_LANGUAGE_STANDARD = gnu11; 673 | GCC_NO_COMMON_BLOCKS = YES; 674 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 675 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 676 | GCC_WARN_UNDECLARED_SELECTOR = YES; 677 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 678 | GCC_WARN_UNUSED_FUNCTION = YES; 679 | GCC_WARN_UNUSED_VARIABLE = YES; 680 | MTL_ENABLE_DEBUG_INFO = NO; 681 | MTL_FAST_MATH = YES; 682 | SWIFT_COMPILATION_MODE = wholemodule; 683 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 684 | }; 685 | name = Release; 686 | }; 687 | 3C140DE12A51953200A8783E /* Debug */ = { 688 | isa = XCBuildConfiguration; 689 | buildSettings = { 690 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 691 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 692 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 693 | CODE_SIGN_ENTITLEMENTS = demo/demo.entitlements; 694 | CODE_SIGN_STYLE = Automatic; 695 | CURRENT_PROJECT_VERSION = 1; 696 | DEVELOPMENT_ASSET_PATHS = "\"demo/Preview Content\""; 697 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 698 | ENABLE_HARDENED_RUNTIME = YES; 699 | ENABLE_PREVIEWS = YES; 700 | GENERATE_INFOPLIST_FILE = YES; 701 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 702 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 703 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 704 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 705 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 706 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 707 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 708 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 709 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 710 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 711 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 712 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 713 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 714 | MACOSX_DEPLOYMENT_TARGET = 11.0; 715 | MARKETING_VERSION = 1.0; 716 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demo; 717 | PRODUCT_NAME = "$(TARGET_NAME)"; 718 | SDKROOT = auto; 719 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; 720 | SUPPORTS_MACCATALYST = YES; 721 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 722 | SWIFT_EMIT_LOC_STRINGS = YES; 723 | SWIFT_VERSION = 5.0; 724 | TARGETED_DEVICE_FAMILY = "1,2,3"; 725 | TVOS_DEPLOYMENT_TARGET = 14.0; 726 | }; 727 | name = Debug; 728 | }; 729 | 3C140DE22A51953200A8783E /* Release */ = { 730 | isa = XCBuildConfiguration; 731 | buildSettings = { 732 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 733 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 734 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 735 | CODE_SIGN_ENTITLEMENTS = demo/demo.entitlements; 736 | CODE_SIGN_STYLE = Automatic; 737 | CURRENT_PROJECT_VERSION = 1; 738 | DEVELOPMENT_ASSET_PATHS = "\"demo/Preview Content\""; 739 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 740 | ENABLE_HARDENED_RUNTIME = YES; 741 | ENABLE_PREVIEWS = YES; 742 | GENERATE_INFOPLIST_FILE = YES; 743 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 744 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 745 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 746 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 747 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 748 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 749 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 750 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 751 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 752 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 753 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 754 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 755 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 756 | MACOSX_DEPLOYMENT_TARGET = 11.0; 757 | MARKETING_VERSION = 1.0; 758 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demo; 759 | PRODUCT_NAME = "$(TARGET_NAME)"; 760 | SDKROOT = auto; 761 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; 762 | SUPPORTS_MACCATALYST = YES; 763 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 764 | SWIFT_EMIT_LOC_STRINGS = YES; 765 | SWIFT_VERSION = 5.0; 766 | TARGETED_DEVICE_FAMILY = "1,2,3"; 767 | TVOS_DEPLOYMENT_TARGET = 14.0; 768 | }; 769 | name = Release; 770 | }; 771 | 3C140DE42A51953200A8783E /* Debug */ = { 772 | isa = XCBuildConfiguration; 773 | buildSettings = { 774 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 775 | BUNDLE_LOADER = "$(TEST_HOST)"; 776 | CODE_SIGN_STYLE = Automatic; 777 | CURRENT_PROJECT_VERSION = 1; 778 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 779 | GENERATE_INFOPLIST_FILE = YES; 780 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 781 | MACOSX_DEPLOYMENT_TARGET = 13.3; 782 | MARKETING_VERSION = 1.0; 783 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demoTests; 784 | PRODUCT_NAME = "$(TARGET_NAME)"; 785 | SDKROOT = auto; 786 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 787 | SWIFT_EMIT_LOC_STRINGS = NO; 788 | SWIFT_VERSION = 5.0; 789 | TARGETED_DEVICE_FAMILY = "1,2"; 790 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/demo"; 791 | }; 792 | name = Debug; 793 | }; 794 | 3C140DE52A51953200A8783E /* Release */ = { 795 | isa = XCBuildConfiguration; 796 | buildSettings = { 797 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 798 | BUNDLE_LOADER = "$(TEST_HOST)"; 799 | CODE_SIGN_STYLE = Automatic; 800 | CURRENT_PROJECT_VERSION = 1; 801 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 802 | GENERATE_INFOPLIST_FILE = YES; 803 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 804 | MACOSX_DEPLOYMENT_TARGET = 13.3; 805 | MARKETING_VERSION = 1.0; 806 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demoTests; 807 | PRODUCT_NAME = "$(TARGET_NAME)"; 808 | SDKROOT = auto; 809 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 810 | SWIFT_EMIT_LOC_STRINGS = NO; 811 | SWIFT_VERSION = 5.0; 812 | TARGETED_DEVICE_FAMILY = "1,2"; 813 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/demo"; 814 | }; 815 | name = Release; 816 | }; 817 | 3C140DE72A51953200A8783E /* Debug */ = { 818 | isa = XCBuildConfiguration; 819 | buildSettings = { 820 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 821 | CODE_SIGN_STYLE = Automatic; 822 | CURRENT_PROJECT_VERSION = 1; 823 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 824 | GENERATE_INFOPLIST_FILE = YES; 825 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 826 | MACOSX_DEPLOYMENT_TARGET = 13.3; 827 | MARKETING_VERSION = 1.0; 828 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demoUITests; 829 | PRODUCT_NAME = "$(TARGET_NAME)"; 830 | SDKROOT = auto; 831 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 832 | SWIFT_EMIT_LOC_STRINGS = NO; 833 | SWIFT_VERSION = 5.0; 834 | TARGETED_DEVICE_FAMILY = "1,2"; 835 | TEST_TARGET_NAME = demo; 836 | }; 837 | name = Debug; 838 | }; 839 | 3C140DE82A51953200A8783E /* Release */ = { 840 | isa = XCBuildConfiguration; 841 | buildSettings = { 842 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 843 | CODE_SIGN_STYLE = Automatic; 844 | CURRENT_PROJECT_VERSION = 1; 845 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 846 | GENERATE_INFOPLIST_FILE = YES; 847 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 848 | MACOSX_DEPLOYMENT_TARGET = 13.3; 849 | MARKETING_VERSION = 1.0; 850 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demoUITests; 851 | PRODUCT_NAME = "$(TARGET_NAME)"; 852 | SDKROOT = auto; 853 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 854 | SWIFT_EMIT_LOC_STRINGS = NO; 855 | SWIFT_VERSION = 5.0; 856 | TARGETED_DEVICE_FAMILY = "1,2"; 857 | TEST_TARGET_NAME = demo; 858 | }; 859 | name = Release; 860 | }; 861 | 3C140E4F2A51C81B00A8783E /* Debug */ = { 862 | isa = XCBuildConfiguration; 863 | buildSettings = { 864 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 865 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 866 | CODE_SIGN_STYLE = Automatic; 867 | CURRENT_PROJECT_VERSION = 1; 868 | DEVELOPMENT_ASSET_PATHS = "\"watchOS-demo Watch App/Preview Content\""; 869 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 870 | ENABLE_PREVIEWS = YES; 871 | GENERATE_INFOPLIST_FILE = YES; 872 | INFOPLIST_KEY_CFBundleDisplayName = "watchOS-demo"; 873 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 874 | INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.iankoex.blurhash.demo.demo; 875 | INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; 876 | LD_RUNPATH_SEARCH_PATHS = ( 877 | "$(inherited)", 878 | "@executable_path/Frameworks", 879 | ); 880 | MARKETING_VERSION = 1.0; 881 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demo.watchkitapp; 882 | PRODUCT_NAME = "$(TARGET_NAME)"; 883 | SDKROOT = watchos; 884 | SKIP_INSTALL = YES; 885 | SWIFT_EMIT_LOC_STRINGS = YES; 886 | SWIFT_VERSION = 5.0; 887 | TARGETED_DEVICE_FAMILY = 4; 888 | WATCHOS_DEPLOYMENT_TARGET = 7.0; 889 | }; 890 | name = Debug; 891 | }; 892 | 3C140E502A51C81B00A8783E /* Release */ = { 893 | isa = XCBuildConfiguration; 894 | buildSettings = { 895 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 896 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 897 | CODE_SIGN_STYLE = Automatic; 898 | CURRENT_PROJECT_VERSION = 1; 899 | DEVELOPMENT_ASSET_PATHS = "\"watchOS-demo Watch App/Preview Content\""; 900 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 901 | ENABLE_PREVIEWS = YES; 902 | GENERATE_INFOPLIST_FILE = YES; 903 | INFOPLIST_KEY_CFBundleDisplayName = "watchOS-demo"; 904 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 905 | INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.iankoex.blurhash.demo.demo; 906 | INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; 907 | LD_RUNPATH_SEARCH_PATHS = ( 908 | "$(inherited)", 909 | "@executable_path/Frameworks", 910 | ); 911 | MARKETING_VERSION = 1.0; 912 | PRODUCT_BUNDLE_IDENTIFIER = com.iankoex.blurhash.demo.demo.watchkitapp; 913 | PRODUCT_NAME = "$(TARGET_NAME)"; 914 | SDKROOT = watchos; 915 | SKIP_INSTALL = YES; 916 | SWIFT_EMIT_LOC_STRINGS = YES; 917 | SWIFT_VERSION = 5.0; 918 | TARGETED_DEVICE_FAMILY = 4; 919 | VALIDATE_PRODUCT = YES; 920 | WATCHOS_DEPLOYMENT_TARGET = 7.0; 921 | }; 922 | name = Release; 923 | }; 924 | 3C140E532A51C81B00A8783E /* Debug */ = { 925 | isa = XCBuildConfiguration; 926 | buildSettings = { 927 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 928 | BUNDLE_LOADER = "$(TEST_HOST)"; 929 | CODE_SIGN_STYLE = Automatic; 930 | CURRENT_PROJECT_VERSION = 1; 931 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 932 | GENERATE_INFOPLIST_FILE = YES; 933 | MARKETING_VERSION = 1.0; 934 | PRODUCT_BUNDLE_IDENTIFIER = "com.iankoex.blurhash.demo.watchOS-demo-Watch-AppTests"; 935 | PRODUCT_NAME = "$(TARGET_NAME)"; 936 | SDKROOT = watchos; 937 | SWIFT_EMIT_LOC_STRINGS = NO; 938 | SWIFT_VERSION = 5.0; 939 | TARGETED_DEVICE_FAMILY = 4; 940 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/watchOS-demo Watch App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/watchOS-demo Watch App"; 941 | WATCHOS_DEPLOYMENT_TARGET = 9.4; 942 | }; 943 | name = Debug; 944 | }; 945 | 3C140E542A51C81B00A8783E /* Release */ = { 946 | isa = XCBuildConfiguration; 947 | buildSettings = { 948 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 949 | BUNDLE_LOADER = "$(TEST_HOST)"; 950 | CODE_SIGN_STYLE = Automatic; 951 | CURRENT_PROJECT_VERSION = 1; 952 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 953 | GENERATE_INFOPLIST_FILE = YES; 954 | MARKETING_VERSION = 1.0; 955 | PRODUCT_BUNDLE_IDENTIFIER = "com.iankoex.blurhash.demo.watchOS-demo-Watch-AppTests"; 956 | PRODUCT_NAME = "$(TARGET_NAME)"; 957 | SDKROOT = watchos; 958 | SWIFT_EMIT_LOC_STRINGS = NO; 959 | SWIFT_VERSION = 5.0; 960 | TARGETED_DEVICE_FAMILY = 4; 961 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/watchOS-demo Watch App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/watchOS-demo Watch App"; 962 | VALIDATE_PRODUCT = YES; 963 | WATCHOS_DEPLOYMENT_TARGET = 9.4; 964 | }; 965 | name = Release; 966 | }; 967 | 3C140E562A51C81B00A8783E /* Debug */ = { 968 | isa = XCBuildConfiguration; 969 | buildSettings = { 970 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 971 | CODE_SIGN_STYLE = Automatic; 972 | CURRENT_PROJECT_VERSION = 1; 973 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 974 | GENERATE_INFOPLIST_FILE = YES; 975 | MARKETING_VERSION = 1.0; 976 | PRODUCT_BUNDLE_IDENTIFIER = "com.iankoex.blurhash.demo.watchOS-demo-Watch-AppUITests"; 977 | PRODUCT_NAME = "$(TARGET_NAME)"; 978 | SDKROOT = watchos; 979 | SWIFT_EMIT_LOC_STRINGS = NO; 980 | SWIFT_VERSION = 5.0; 981 | TARGETED_DEVICE_FAMILY = 4; 982 | TEST_TARGET_NAME = "watchOS-demo Watch App"; 983 | WATCHOS_DEPLOYMENT_TARGET = 9.4; 984 | }; 985 | name = Debug; 986 | }; 987 | 3C140E572A51C81B00A8783E /* Release */ = { 988 | isa = XCBuildConfiguration; 989 | buildSettings = { 990 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 991 | CODE_SIGN_STYLE = Automatic; 992 | CURRENT_PROJECT_VERSION = 1; 993 | DEVELOPMENT_TEAM = X7ZCGTWN8T; 994 | GENERATE_INFOPLIST_FILE = YES; 995 | MARKETING_VERSION = 1.0; 996 | PRODUCT_BUNDLE_IDENTIFIER = "com.iankoex.blurhash.demo.watchOS-demo-Watch-AppUITests"; 997 | PRODUCT_NAME = "$(TARGET_NAME)"; 998 | SDKROOT = watchos; 999 | SWIFT_EMIT_LOC_STRINGS = NO; 1000 | SWIFT_VERSION = 5.0; 1001 | TARGETED_DEVICE_FAMILY = 4; 1002 | TEST_TARGET_NAME = "watchOS-demo Watch App"; 1003 | VALIDATE_PRODUCT = YES; 1004 | WATCHOS_DEPLOYMENT_TARGET = 9.4; 1005 | }; 1006 | name = Release; 1007 | }; 1008 | /* End XCBuildConfiguration section */ 1009 | 1010 | /* Begin XCConfigurationList section */ 1011 | 3C140DB62A51952F00A8783E /* Build configuration list for PBXProject "demo" */ = { 1012 | isa = XCConfigurationList; 1013 | buildConfigurations = ( 1014 | 3C140DDE2A51953200A8783E /* Debug */, 1015 | 3C140DDF2A51953200A8783E /* Release */, 1016 | ); 1017 | defaultConfigurationIsVisible = 0; 1018 | defaultConfigurationName = Release; 1019 | }; 1020 | 3C140DE02A51953200A8783E /* Build configuration list for PBXNativeTarget "demo" */ = { 1021 | isa = XCConfigurationList; 1022 | buildConfigurations = ( 1023 | 3C140DE12A51953200A8783E /* Debug */, 1024 | 3C140DE22A51953200A8783E /* Release */, 1025 | ); 1026 | defaultConfigurationIsVisible = 0; 1027 | defaultConfigurationName = Release; 1028 | }; 1029 | 3C140DE32A51953200A8783E /* Build configuration list for PBXNativeTarget "demoTests" */ = { 1030 | isa = XCConfigurationList; 1031 | buildConfigurations = ( 1032 | 3C140DE42A51953200A8783E /* Debug */, 1033 | 3C140DE52A51953200A8783E /* Release */, 1034 | ); 1035 | defaultConfigurationIsVisible = 0; 1036 | defaultConfigurationName = Release; 1037 | }; 1038 | 3C140DE62A51953200A8783E /* Build configuration list for PBXNativeTarget "demoUITests" */ = { 1039 | isa = XCConfigurationList; 1040 | buildConfigurations = ( 1041 | 3C140DE72A51953200A8783E /* Debug */, 1042 | 3C140DE82A51953200A8783E /* Release */, 1043 | ); 1044 | defaultConfigurationIsVisible = 0; 1045 | defaultConfigurationName = Release; 1046 | }; 1047 | 3C140E4E2A51C81B00A8783E /* Build configuration list for PBXNativeTarget "watchOS-demo Watch App" */ = { 1048 | isa = XCConfigurationList; 1049 | buildConfigurations = ( 1050 | 3C140E4F2A51C81B00A8783E /* Debug */, 1051 | 3C140E502A51C81B00A8783E /* Release */, 1052 | ); 1053 | defaultConfigurationIsVisible = 0; 1054 | defaultConfigurationName = Release; 1055 | }; 1056 | 3C140E522A51C81B00A8783E /* Build configuration list for PBXNativeTarget "watchOS-demo Watch AppTests" */ = { 1057 | isa = XCConfigurationList; 1058 | buildConfigurations = ( 1059 | 3C140E532A51C81B00A8783E /* Debug */, 1060 | 3C140E542A51C81B00A8783E /* Release */, 1061 | ); 1062 | defaultConfigurationIsVisible = 0; 1063 | defaultConfigurationName = Release; 1064 | }; 1065 | 3C140E552A51C81B00A8783E /* Build configuration list for PBXNativeTarget "watchOS-demo Watch AppUITests" */ = { 1066 | isa = XCConfigurationList; 1067 | buildConfigurations = ( 1068 | 3C140E562A51C81B00A8783E /* Debug */, 1069 | 3C140E572A51C81B00A8783E /* Release */, 1070 | ); 1071 | defaultConfigurationIsVisible = 0; 1072 | defaultConfigurationName = Release; 1073 | }; 1074 | /* End XCConfigurationList section */ 1075 | 1076 | /* Begin XCSwiftPackageProductDependency section */ 1077 | 3C140DEC2A51960200A8783E /* UnifiedBlurHash */ = { 1078 | isa = XCSwiftPackageProductDependency; 1079 | productName = UnifiedBlurHash; 1080 | }; 1081 | 3C140E5A2A51C8CC00A8783E /* UnifiedBlurHash */ = { 1082 | isa = XCSwiftPackageProductDependency; 1083 | productName = UnifiedBlurHash; 1084 | }; 1085 | /* End XCSwiftPackageProductDependency section */ 1086 | }; 1087 | rootObject = 3C140DB32A51952F00A8783E /* Project object */; 1088 | } 1089 | -------------------------------------------------------------------------------- /demo/demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/demo.xcodeproj/xcshareddata/xcschemes/demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 36 | 42 | 43 | 44 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /demo/demo.xcodeproj/xcshareddata/xcschemes/watchOS-demo Watch App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 50 | 56 | 57 | 58 | 61 | 67 | 68 | 69 | 70 | 71 | 81 | 83 | 89 | 90 | 91 | 92 | 98 | 100 | 106 | 107 | 108 | 109 | 111 | 112 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /demo/demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /demo/demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/demo/Assets.xcassets/sunflower.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sunflower.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo/demo/Assets.xcassets/sunflower.imageset/sunflower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iankoex/UnifiedBlurHash/675d79835d6338e3fd4a314ef3220ae41f7eb6d4/demo/demo/Assets.xcassets/sunflower.imageset/sunflower.jpg -------------------------------------------------------------------------------- /demo/demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // demo 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | import UnifiedBlurHash 10 | 11 | struct ContentView: View { 12 | @State private var imageString = "LWC$.Wx]Sjof~Wx]X9oe?btRofax" 13 | @State private var imageDecodedFromBlurHashString: UnifiedImage? = nil 14 | @State private var averageColorFromBlurHashString: Color? 15 | 16 | var body: some View { 17 | List { 18 | Section(content: { 19 | Button("Encode", action: encode) 20 | 21 | Button("Decode", action: decode) 22 | }) 23 | 24 | Section(content: { 25 | Image("sunflower") 26 | .resizable() 27 | .aspectRatio(9/16, contentMode: .fit) 28 | .frame(maxWidth: 300) 29 | }, header: { 30 | Text("Original Image") 31 | }) 32 | 33 | Section(content: { 34 | if let imageDecodedFromBlurHashString { 35 | Image(unifiedImage: imageDecodedFromBlurHashString) 36 | .resizable() 37 | .aspectRatio(9/16, contentMode: .fit) 38 | .frame(maxWidth: 300) 39 | } 40 | }, header: { 41 | Text("Image Decoded From BlurHash String") 42 | }) 43 | 44 | Section(content: { 45 | averageColorFromBlurHashString?.frame(width: 300, height: 300) 46 | }, header: { 47 | Text("Average Color From BlurHash String") 48 | }) 49 | } 50 | } 51 | 52 | private func encode() { 53 | Task { 54 | // https://www.pexels.com/photo/person-holding-yellow-sunflower-1624076/ 55 | let image = UnifiedImage(named: "sunflower") 56 | guard let image = image else { 57 | return 58 | } 59 | let str = await UnifiedBlurHash.getBlurHashString(from: image) 60 | guard let str = str else { 61 | return 62 | } 63 | DispatchQueue.main.async { 64 | self.imageString = str 65 | } 66 | } 67 | } 68 | 69 | private func decode() { 70 | Task { 71 | let image = await UnifiedBlurHash.getUnifiedImage(from: imageString) 72 | self.averageColorFromBlurHashString = await UnifiedBlurHash.getAverageColor(from: imageString) 73 | guard let image = image else { 74 | return 75 | } 76 | DispatchQueue.main.async { 77 | self.imageDecodedFromBlurHashString = image 78 | } 79 | } 80 | } 81 | } 82 | 83 | struct ContentView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | ContentView() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /demo/demo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/demo/demo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/demo/demoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // demoApp.swift 3 | // demo 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct demoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/demoTests/demoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // demoTests.swift 3 | // demoTests 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class demoTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | // Any test you write for XCTest can be annotated as throws and async. 24 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 25 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /demo/demoUITests/demoUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // demoUITests.swift 3 | // demoUITests 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class demoUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demo/demoUITests/demoUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // demoUITestsLaunchTests.swift 3 | // demoUITests 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class demoUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "watchos", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/Assets.xcassets/sunflower.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pexels-lil-artsy-1624076-2.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/Assets.xcassets/sunflower.imageset/pexels-lil-artsy-1624076-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iankoex/UnifiedBlurHash/675d79835d6338e3fd4a314ef3220ae41f7eb6d4/demo/watchOS-demo Watch App/Assets.xcassets/sunflower.imageset/pexels-lil-artsy-1624076-2.jpg -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // demo 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | import UnifiedBlurHash 10 | 11 | struct ContentView: View { 12 | @State private var imageString = "LWC$.Wx]Sjof~Wx]X9oe?btRofax" 13 | @State private var image: UnifiedImage? = nil 14 | 15 | var body: some View { 16 | ScrollView { 17 | VStack(spacing: 20) { 18 | Button("Encode", action: encode) 19 | 20 | Button("Decode", action: decode) 21 | 22 | Image("sunflower") 23 | .resizable() 24 | .aspectRatio(9/16, contentMode: .fit) 25 | .frame(maxWidth: 300) 26 | 27 | if image != nil { 28 | Image(unifiedImage: image!) 29 | .resizable() 30 | .aspectRatio(9/16, contentMode: .fit) 31 | .frame(maxWidth: 300) 32 | } 33 | } 34 | .padding(.horizontal) 35 | } 36 | } 37 | 38 | private func encode() { 39 | Task { 40 | // https://www.pexels.com/photo/person-holding-yellow-sunflower-1624076/ 41 | let image = UnifiedImage(named: "sunflower") 42 | guard let image = image else { 43 | return 44 | } 45 | let str = await UnifiedBlurHash.getBlurHashString(from: image) 46 | guard let str = str else { 47 | return 48 | } 49 | DispatchQueue.main.async { 50 | self.imageString = str 51 | } 52 | } 53 | } 54 | 55 | private func decode() { 56 | Task { 57 | let image = await UnifiedBlurHash.getUnifiedImage(from: imageString) 58 | guard let image = image else { 59 | return 60 | } 61 | DispatchQueue.main.async { 62 | self.image = image 63 | } 64 | } 65 | } 66 | } 67 | 68 | struct ContentView_Previews: PreviewProvider { 69 | static var previews: some View { 70 | ContentView() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch App/watchOS_demoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // watchOS_demoApp.swift 3 | // watchOS-demo Watch App 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct watchOS_demo_Watch_AppApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch AppTests/watchOS_demo_Watch_AppTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // watchOS_demo_Watch_AppTests.swift 3 | // watchOS-demo Watch AppTests 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import watchOS_demo_Watch_App 10 | 11 | final class watchOS_demo_Watch_AppTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Tests marked async will run the test method on an arbitrary thread managed by the Swift runtime. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch AppUITests/watchOS_demo_Watch_AppUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // watchOS_demo_Watch_AppUITests.swift 3 | // watchOS-demo Watch AppUITests 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class watchOS_demo_Watch_AppUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demo/watchOS-demo Watch AppUITests/watchOS_demo_Watch_AppUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // watchOS_demo_Watch_AppUITestsLaunchTests.swift 3 | // watchOS-demo Watch AppUITests 4 | // 5 | // Created by Ian on 02/07/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class watchOS_demo_Watch_AppUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | --------------------------------------------------------------------------------