Skip to content

Commit

Permalink
feat: EDRQRCodeView
Browse files Browse the repository at this point in the history
  • Loading branch information
tsuzukihashi committed Aug 18, 2023
1 parent 71ecc74 commit 1ed071e
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 89 deletions.
95 changes: 7 additions & 88 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,90 +1,9 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
29 changes: 29 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "EDR_Swift",
platforms: [.iOS(.v16)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "EDR_Swift",
targets: ["EDR_Swift"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "EDR_Swift",
dependencies: []),
.testTarget(
name: "EDR_SwiftTests",
dependencies: ["EDR_Swift"]),
]
)
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# EDR_Swift
I want to use EDR features easily.

A description of this package.
19 changes: 19 additions & 0 deletions Sources/EDR_Swift/EDRQRCodeView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import SwiftUI

@available(iOS 16.0, *)
public struct EDRQRCodeView: View {
@StateObject var viewModel: EDRQRCodeViewModel

init(qrCodeTextContent: String, imageRenderSize: CGSize) {
_viewModel = StateObject(
wrappedValue: .init(
qrCodeTextContent: qrCodeTextContent,
imageRenderSize: imageRenderSize
)
)
}

public var body: some View {
MetalKitView(renderer: viewModel.renderer)
}
}
50 changes: 50 additions & 0 deletions Sources/EDR_Swift/EDRQRCodeViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import SwiftUI

@available(iOS 16.0, *)
public class EDRQRCodeViewModel: ObservableObject {
@Published var renderer: Renderer

public init(
qrCodeTextContent: String,
imageRenderSize: CGSize
) {
self.renderer = Renderer(imageProvider: { (scaleFactor: CGFloat, headroom: CGFloat) -> CIImage? in
guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") else {
return nil
}

let inputData = qrCodeTextContent.data(using: .utf8)
qrFilter.setValue(inputData, forKey: "inputMessage")
qrFilter.setValue("H", forKey: "inputCorrectionLevel")

guard var image = qrFilter.outputImage else { return nil }

let sizeTransform = CGAffineTransform(
scaleX: imageRenderSize.width * (scaleFactor / image.extent.size.width),
y: imageRenderSize.height * (scaleFactor / image.extent.size.height)
)

image = image.transformed(by: sizeTransform)

let maxRGB = headroom
guard let colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
let maxFillColor = CIColor(red: maxRGB, green: maxRGB, blue: maxRGB, colorSpace: colorSpace) else {
return nil
}

let fillImage = CIImage(color: maxFillColor)
let maskFilter = CIFilter.blendWithMask()
maskFilter.maskImage = image
maskFilter.inputImage = fillImage

return maskFilter.outputImage?.cropped(
to: CGRect(
x: 0,
y: 0,
width: imageRenderSize.width * scaleFactor,
height: imageRenderSize.height * scaleFactor
)
)
})
}
}
31 changes: 31 additions & 0 deletions Sources/EDR_Swift/MetalKitView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import SwiftUI
import MetalKit

@available(iOS 16.0, *)
public struct MetalKitView: UIViewRepresentable {
@StateObject public var renderer: Renderer

public func makeUIView(context: Context) -> MTKView {
let view = MTKView(frame: .zero, device: renderer.device)
view.preferredFramesPerSecond = 10
view.framebufferOnly = false
view.delegate = renderer

if let layer = view.layer as? CAMetalLayer {
layer.wantsExtendedDynamicRangeContent = true
layer.colorspace = CGColorSpace(
name: CGColorSpace.extendedLinearDisplayP3
)
view.colorPixelFormat = MTLPixelFormat.rgba16Float
}
return view
}

public func updateUIView(_ view: MTKView, context: Context) {
configure(view: view, using: renderer)
}

private func configure(view: MTKView, using renderer: Renderer) {
view.delegate = renderer
}
}
39 changes: 39 additions & 0 deletions Sources/EDR_Swift/QRCodeGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import CoreImage.CIFilterBuiltins
import class UIKit.UIImage

@available(iOS 16.0, *)
enum QRCodeGenerator {
static func generate(from contents: String) -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(contents.utf8)
filter.correctionLevel = "H"

if let outputImage = filter.outputImage,
let cgImage = context.createCGImage(
outputImage,
from: outputImage.extent
) {
return UIImage(cgImage: cgImage)
}

return nil
}

static func generate(from contents: String) -> CIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(contents.utf8)
filter.correctionLevel = "H"

if let outputImage = filter.outputImage,
let cgImage = context.createCGImage(
outputImage,
from: outputImage.extent
) {
return CIImage(cgImage: cgImage)
}

return nil
}
}
104 changes: 104 additions & 0 deletions Sources/EDR_Swift/Renderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Metal
import MetalKit
import CoreImage

@available(iOS 16.0, *)
public final class Renderer: NSObject, ObservableObject {
@Published var currentHeadroom: CGFloat = 1.0

public let device: MTLDevice? = MTLCreateSystemDefaultDevice()
private let commandQueue: MTLCommandQueue?
private let renderContext: CIContext?
private let renderQueue = DispatchSemaphore(value: 3)

private let imageProvider: (_ contentScaleFactor: CGFloat, _ headroom: CGFloat) -> CIImage?

public init(imageProvider: @escaping (_ contentScaleFactor: CGFloat, _ headroom: CGFloat) -> CIImage?) {
self.imageProvider = imageProvider
self.commandQueue = self.device?.makeCommandQueue()
if let commandQueue {
self.renderContext = CIContext(
mtlCommandQueue: commandQueue,
options: [
.name: "Renderer",
.cacheIntermediates: true,
.allowLowPower: true
]
)
} else {
self.renderContext = nil
}
super.init()
}
}

extension Renderer: MTKViewDelegate {
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
// Respond to drawable size or orientation changes.
}

public func draw(in view: MTKView) {
guard let commandQueue else { return }
// wait for previous render to complete
_ = renderQueue.wait(timeout: DispatchTime.distantFuture)

if let commandBuffer = commandQueue.makeCommandBuffer() {
commandBuffer.addCompletedHandler { (_ commandBuffer)-> Swift.Void in
self.renderQueue.signal()
}

if let drawable = view.currentDrawable {

let drawSize = view.drawableSize
let contentScaleFactor = view.contentScaleFactor
let destination = CIRenderDestination(
width: Int(drawSize.width),
height: Int(drawSize.height),
pixelFormat: view.colorPixelFormat,
commandBuffer: commandBuffer,
mtlTextureProvider: { () -> MTLTexture in
return drawable.texture
})

// calculate the maximum supported EDR value (headroom)
var headroom = CGFloat(1.0)
headroom = view.window?.screen.currentEDRHeadroom ?? 1.0
currentHeadroom = headroom

// Get the CI image to be displayed from the delegate function
guard var image = self.imageProvider(contentScaleFactor, headroom) else {
return
}

// Center the image in the view's visible area.
let iRect = image.extent
let backBounds = CGRect(
x: 0,
y: 0,
width: drawSize.width,
height: drawSize.height
)
let shiftX = round((backBounds.size.width + iRect.origin.x - iRect.size.width) * 0.5)
let shiftY = round((backBounds.size.height + iRect.origin.y - iRect.size.height) * 0.5)

image = image.transformed(by: CGAffineTransform(translationX: shiftX, y: shiftY))

// provide a background if the image is transparent
image = image.composited(over: .gray)

// Start a task that renders to the texture destination.
guard let renderContext else { return }
_ = try? renderContext.startTask(
toRender: image,
from: backBounds,
to: destination,
at: CGPoint.zero
)

// show rendered work and commit render task
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
}
}
6 changes: 6 additions & 0 deletions Tests/EDR_SwiftTests/EDR_SwiftTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import XCTest
@testable import EDR_Swift

final class EDR_SwiftTests: XCTestCase {

}

0 comments on commit 1ed071e

Please sign in to comment.