100 Days of SwiftUI(DAY 83(Project16 part 5))

QRコードの生成と拡大
Generating and scaling up a QR code

Core Imageを使用すると、任意の入力文字列からQRコードを生成でき、非常に迅速に生成できます。ただし、問題があります。生成される画像は、データを表示するために必要なピクセルと同じ大きさしかないため、非常に小さくなります。QRコードを大きくするのは簡単ですが、見栄えを良くするには、SwiftUIの画像補間も調整する必要があります。したがって、このステップでは、フォームに名前と電子メールアドレスを入力するようユーザーに要求し、これら2つの情報を使用してそれらを識別するQRコードを生成し、あいまいにせずにコードを拡大します。

名前と電子メールアドレスのフィールドを使用して、QRコードを生成します。QRコードは、電話やその他のデバイスでスキャンできる白黒ピクセルの正方形のコレクションです。Core Imageにはこの組み込みのフィルターがあり、以前にCore Imageフィルターの使用方法を学習したので、これは非常に似ていることがわかります。

textContentType()これは、ユーザーに要求する情報の種類をiOSに通知します。これにより、iOSがユーザーに代わってオートコンプリートデータを提供できるようになり、アプリがより使いやすくなります。

QRコードやバーコードなどのラインアートは、画像の補間を無効にするのに最適です。
.interpolation(.none)

//  MeView.swift
//  HotProspects

import SwiftUI
import CoreImage.CIFilterBuiltins

struct MeView: View {
    @State private var name = "Anonymous"
    @State private var emailAddress = "you@yoursite.com"
    
    let context = CIContext()
    let filter = CIFilter.qrCodeGenerator()
    
    func generateQRCode(from string: String) -> UIImage {
        let data = Data(string.utf8)
        filter.setValue(data, forKey: "inputMessage")

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

        return UIImage(systemName: "xmark.circle") ?? UIImage()
    }
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Name", text: $name)
                    .textContentType(.name)
                    .font(.title)
                    .padding(.horizontal)

                TextField("Email address", text: $emailAddress)
                    .textContentType(.emailAddress)
                    .font(.title)
                    .padding([.horizontal, .bottom])
                Image(uiImage: generateQRCode(from: "\(name)\n\(emailAddress)"))
                    .interpolation(.none)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200, height: 200)
                
                Spacer()
            }
            .navigationBarTitle("Your code")
        }
    }
}

SwiftUIでQRコードをスキャンする
Scanning QR codes with SwiftUI

QRコード、または実際にはバーコードなどのあらゆる種類の可視コードのスキャンは、AppleのAVFoundationライブラリで実行できます。これはSwiftUIにひどくスムーズに統合されないので、かなりの苦労をスキップするために、QcodeコードリーダーをSwiftパッケージにパッケージ化して、Xcode内に直接追加して使用できるようにしました。

パッケージリポジトリのURLとしてhttps://github.com/twostraws/CodeScannerを入力します。

//
//  ProspectsView.swift
//  HotProspects
//
//  Created by Naoki Abe on 2020/09/01.
//  Copyright © 2020 Naoki Abe. All rights reserved.
//

import SwiftUI
import CodeScanner

struct ProspectsView: View {
    @EnvironmentObject var prospects: Prospects
    
    @State private var isShowingScanner = false
    
    enum FilterType {
        case none, contacted, uncontacted
    }
    
    let filter: FilterType
    
    var title: String {
        switch filter {
        case .none:
            return "Everyone"
        case .contacted:
            return "Contacted people"
        case .uncontacted:
            return "Uncontacted people"
        }
    }
    
    var filteredProspects: [Prospect] {
        switch filter {
        case .none:
            return prospects.people
        case .contacted:
            return prospects.people.filter { $0.isContacted }
        case .uncontacted:
            return prospects.people.filter { !$0.isContacted }
        }
    }
    
    func handleScan(result: Result<String, CodeScannerView.ScanError>) {
       self.isShowingScanner = false
       // more code to come
        switch result {
        case .success(let code):
            let details = code.components(separatedBy: "\n")
            guard details.count == 2 else { return }

            let person = Prospect()
            person.name = details[0]
            person.emailAddress = details[1]

            self.prospects.people.append(person)
        case .failure(let error):
            print("Scanning failed")
        }
    }
    
    var body: some View {
        NavigationView {
                // Text("People: \(prospects.people.count)")
                List {
                    ForEach(filteredProspects) { prospect in
                        VStack(alignment: .leading) {
                            Text(prospect.name)
                                .font(.headline)
                            Text(prospect.emailAddress)
                                .foregroundColor(.secondary)
                        }
                    }
                }
                .navigationBarTitle(title)
                .navigationBarItems(trailing: Button(action: {
                    self.isShowingScanner = true
//                    let prospect = Prospect()
//                    prospect.name = "Paul Hudson"
//                    prospect.emailAddress = "paul@hackingwithswift.com"
//                    self.prospects.people.append(prospect)
                }) {
                    Image(systemName: "qrcode.viewfinder")
                    Text("Scan")
                })
                .sheet(isPresented: $isShowingScanner) {
                    CodeScannerView(codeTypes: [.qr], simulatedData: "Paul Hudson\npaul@hackingwithswift.com", completion: self.handleScan)
                }
        }
    }
}

コンテキストメニューを使用したオプションの追加
Adding options with a context menu

この問題は、のpeople配列がProspectsでマークされているために発生@Publishedします。

つまり、その配列に対してアイテムを追加または削除すると、変更通知が送信されます。

ただし、配列内のアイテムを静かに変更すると、SwiftUIはその変更を検出せず、ビューは更新されません。

これを修正するには、重要な変更があったことを手動でSwiftUIに通知する必要があります。したがって、でブール値を反転するのProspectsViewではなく、代わりにProspectsクラスのメソッドを呼び出して同じブール値を反転させ、同時に変更通知を送信します。

まず、このメソッドをProspectsクラスに追加します。


func toggle(_ prospect: Prospect) {
    objectWillChange.send()
    prospect.isContacted.toggle()
}


//
//  Prospect.swift
//  HotProspects
//
//  Created by Naoki Abe on 2020/09/01.
//  Copyright © 2020 Naoki Abe. All rights reserved.
//

import SwiftUI

class Prospect: Identifiable, Codable {
    let id = UUID()
    var name = "Anonymous"
    var emailAddress = ""
    //var isContacted = false
    fileprivate(set) var isContacted = false
}

class Prospects: ObservableObject {
    @Published var people: [Prospect]

    init() {
        self.people = []
    }
    
    func toggle(_ prospect: Prospect) {
        objectWillChange.send()
        prospect.isContacted.toggle()
    }
}


//
//  ProspectsView.swift
//  HotProspects


import SwiftUI
import CodeScanner

struct ProspectsView: View {
    @EnvironmentObject var prospects: Prospects
    
    @State private var isShowingScanner = false
    
    enum FilterType {
        case none, contacted, uncontacted
    }
    
    let filter: FilterType
    
    var title: String {
        switch filter {
        case .none:
            return "Everyone"
        case .contacted:
            return "Contacted people"
        case .uncontacted:
            return "Uncontacted people"
        }
    }
    
    var filteredProspects: [Prospect] {
        switch filter {
        case .none:
            return prospects.people
        case .contacted:
            return prospects.people.filter { $0.isContacted }
        case .uncontacted:
            return prospects.people.filter { !$0.isContacted }
        }
    }
    
    func handleScan(result: Result<String, CodeScannerView.ScanError>) {
       self.isShowingScanner = false
       // more code to come
        switch result {
        case .success(let code):
            let details = code.components(separatedBy: "\n")
            guard details.count == 2 else { return }

            let person = Prospect()
            person.name = details[0]
            person.emailAddress = details[1]

            self.prospects.people.append(person)
        case .failure(let error):
            print("Scanning failed")
        }
    }
    
    var body: some View {
        NavigationView {
                // Text("People: \(prospects.people.count)")
                List {
                    ForEach(filteredProspects) { prospect in
                        VStack(alignment: .leading) {
                            Text(prospect.name)
                                .font(.headline)
                            Text(prospect.emailAddress)
                                .foregroundColor(.secondary)
                        }
                        .contextMenu {
                            Button(prospect.isContacted ? "Mark Uncontacted" : "Mark Contacted" ) {
                                //prospect.isContacted.toggle()
                                self.prospects.toggle(prospect)
                            }
                        }
                    }
                }
                .navigationBarTitle(title)
                .navigationBarItems(trailing: Button(action: {
                    self.isShowingScanner = true
//                    let prospect = Prospect()
//                    prospect.name = "Paul Hudson"
//                    prospect.emailAddress = "paul@hackingwithswift.com"
//                    self.prospects.people.append(prospect)
                }) {
                    Image(systemName: "qrcode.viewfinder")
                    Text("Scan")
                })
                .sheet(isPresented: $isShowingScanner) {
                    CodeScannerView(codeTypes: [.qr], simulatedData: "Paul Hudson\npaul@hackingwithswift.com", completion: self.handleScan)
                }
        }
    }
}