20200927のiOSに関する記事は5件です。

iOS14 encrypted DNS(DNS over TLS, DNS over HTTPS)

概要と注意事項

  • iOS14から追加されたDNSSettingsペイロードによる暗号化DNS利用プロファイルの例
    • macOS11からでも使えるっぽい
  • DNSフィルタリングと組み合わせれば簡単なフィルタリングがOS標準機能で実現できる
    • 例えばサンプルのDoHの方であれば、エロサイトを(完全ではないだろうが)ブロックできる
    • 解除できちゃうからやっぱりSupervisorでガチガチにするしかねぇ
  • ProhibitDisablementがtrueの場合は要Supervisor
    • falseの場合、「設定→一般→VPNとネットワーク」で変更が可能
  • ちゃんと切り替わっているかはCloudflareのDebug Informationでしか見ていない
  • 値を書き換えればGoogle Public DNSIIS Public DNSは使えたっぽい
    • こちらはちゃんと切り替わっているかの裏まではきちんと確認していない
  • 上のリファレンスのうち最低限しか記述していないため、すべての通信のDNSが切り替わる
    • 特定のDNSサーバでしか名前解決できないようなものがあるともちろんコケる
      • 社内Wi-Fiとかキャリアサービスとか、VVMとMMS辺りが怪しげ
    • OnDemandRulesを書けば「モバイルの時だけ」とか「このWi-Fi APに繋いだ時だけ」とかルールが作れるっぽい
  • 複数設定が入る
    • サンプルの二つはPayloadIdentifierが同一なので互いに上書きし合う(重複せずどちらか一方)
    • 値を書き換えて複数入れ、切り替えは可能
    • 設定は最後に入れたものに切り替わるっぽい
    • 一つのプロファイルに複数のDNS設定を記載することも可能
  • これを入れたから通信速度が速くなるとかそんなことを期待してはいけない
  • 人柱になりたい奇特な方がいれば、末尾の「参考元とか」リンクからプロファイルの入手が可能
  • どうでもいい話、Android EnterpriseでもAPI 29(Android 10)から同様の機能あり
  • プロファイルを入れる方法でなく、アプリから制御する方法もあるっぽい(NextDNSがこれ?)
  • Wi-Fi設定の警告が消せるかもしれないけど未確認

DNS over TLS

Cloudflare 1.1.1.1 のTLSを利用する場合

DoT_Cloudflare.mobileconfig
<?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>PayloadContent</key>
        <array>
            <dict>
                <key>PayloadDescription</key>
                <string>DNS Settings</string>
                <key>PayloadDisplayName</key>
                <string>DNS設定(DoT)</string>
                <key>PayloadIdentifier</key>
                <string>com.apple.dnsSettings.managed.91C836B1-5749-4C12-B613-384008F96D92</string>
                <key>PayloadType</key>
                <string>com.apple.dnsSettings.managed</string>
                <key>PayloadUUID</key>
                <string>91C836B1-5749-4C12-B613-384008F96D92</string>
                <key>PayloadVersion</key>
                <integer>1</integer>
                <key>DNSSettings</key>
                <dict>
                    <key>DNSProtocol</key>
                    <string>TLS</string>
                    <key>ServerAddresses</key>
                    <array>
                        <string>2606:4700:4700::1111</string>
                        <string>2606:4700:4700::1001</string>
                        <string>1.1.1.1</string>
                        <string>1.0.0.1</string>
                    </array>
                    <key>ServerName</key>
                    <string>cloudflare-dns.com</string>
                </dict>
                <key>ProhibitDisablement</key>
                <false/>
            </dict>
        </array>
        <key>PayloadDisplayName</key>
        <string>DNSSettings</string>
        <key>PayloadIdentifier</key>
        <string>com.apple.dnsSettings.managed</string>
        <key>PayloadRemovalDisallowed</key>
        <false/>
        <key>PayloadType</key>
        <string>Configuration</string>
        <key>PayloadUUID</key>
        <string>2C3575D8-8480-4AD0-B6A9-803D9FBEDB5E</string>
        <key>PayloadVersion</key>
        <integer>1</integer>
    </dict>
</plist>

DNS over HTTPS

Cloudflare 1.1.1.1 for Families(Block Malware And Adult Content) のHTTPSを利用する場合

DoH_Cloudflare_Families3.mobileconfig
<?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>PayloadContent</key>
    <array>
        <dict>
            <key>PayloadDescription</key>
            <string>DNS Settings</string>
            <key>PayloadDisplayName</key>
            <string>DNS設定(DoH)</string>
            <key>PayloadIdentifier</key>
            <string>com.apple.dnsSettings.managed.91C836B1-5749-4C12-B613-384008F96D92</string>
            <key>PayloadType</key>
            <string>com.apple.dnsSettings.managed</string>
            <key>PayloadUUID</key>
            <string>91C836B1-5749-4C12-B613-384008F96D92</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
            <key>DNSSettings</key>
            <dict>
                <key>DNSProtocol</key>
                <string>HTTPS</string>
                <key>ServerURL</key>
                <string>https://family.cloudflare-dns.com/dns-query</string>
            </dict>
            <key>ProhibitDisablement</key>
            <false/>
        </dict>
    </array>
    <key>PayloadDisplayName</key>
    <string>DNSSettings</string>
    <key>PayloadIdentifier</key>
    <string>com.apple.dnsSettings.managed</string>
    <key>PayloadRemovalDisallowed</key>
    <false/>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>2C3575D8-8480-4AD0-B6A9-803D9FBEDB5E</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>

キーと値

  • PayloadDisplayName
    • DNS設定名、切替画面のリストとプロファイル内での表示
  • DNSProtocol
    • DoTならばTLS、DoHならHTTPS
  • DoT用設定値
    • ServerAddresses
      • DNSサーバのIPアドレスを記載する(ホスト名でも通るがリファレンス上は「IPv4 and IPv6 addresses.」)
    • ServerName
      • たぶんサーバ証明書のSANs
  • DoH用設定値
    • ServerURL
      • クエリを投げる先のURL
  • ProhibitDisablement

    • 手動変更を禁止するのであればtrue
  • PayloadDisplayName

    • プロファイル名

組織名とか説明書きとかその他諸々は省略

参考元とか

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでフォトライブラリ

SwiftUIでフォトライブラリを表示する

ボタンをタップして、フォトライブラリを表示します。
フォトライブラリで写真を選択すると、全画面に表示します。

環境

Swift 5.3
Xcode 12.0.1
macOS 10.15.7

フォトライブラリ

フォトライブラリをSwiftUIでラップした構造体を定義します。

ImagePicker.swift
import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    var sourceType: UIImagePickerController.SourceType = .photoLibrary

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        imagePicker.allowsEditing = false
        imagePicker.sourceType = sourceType
        return imagePicker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

    }
}

最初の画面

アプリの基本となる画面の構造体です。
ステート変数の基づいて、フォトライブラリをシート形式で表示します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var image = UIImage()
    @State private var isShowPhotoLibrary = false

    var body: some View {
        VStack {
            Image(uiImage: self.image)
            Button(action: {
                self.isShowPhotoLibrary = true
            }, label: {
                Text("Photo Library")
                    .padding()
            })
        }
        .sheet(isPresented: $isShowPhotoLibrary, content: {
            ImagePicker(sourceType: .photoLibrary)
        })
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

フォトライブラリの写真を選択する

UIImagePickerControllerDelegateプロトコルに準拠します。

ImagePicker.swift
import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    // MARK: - Working with UIViewControllerRepresentable
    var sourceType: UIImagePickerController.SourceType = .photoLibrary

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        imagePicker.allowsEditing = false
        imagePicker.sourceType = sourceType
        imagePicker.delegate = context.coordinator  // Coordinater to adopt UIImagePickerControllerDelegate Protcol.
        return imagePicker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

    }

    // MARK: - Using Coordinator to Adopt the UIImagePickerControllerDelegate Protocol
    @Binding var selectedImage: UIImage
    @Environment(\.presentationMode) private var presentationMode

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: ImagePicker

        init(_ parent: ImagePicker) {
            self.parent = parent
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

            if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
                parent.selectedImage = image
            }

            parent.presentationMode.wrappedValue.dismiss()
        }
    }
}

選択した写真を表示する

Imageビューにモディファイアを追加します。
画面遷移するタイミングで、選択した写真をimageプロパティに設定します。

ContentView.swift
struct ContentView: View {
    @State private var image = UIImage()
    @State private var isShowPhotoLibrary = false

    var body: some View {
        VStack {
            Image(uiImage: self.image)
                .resizable()
                .scaledToFill()
                .frame(minWidth: 0, maxWidth: .infinity)
                .edgesIgnoringSafeArea(.all)
            Button(action: {
                self.isShowPhotoLibrary = true
            }, label: {
                Text("Photo Library")
                    .padding()
            })
        }
        .sheet(isPresented: $isShowPhotoLibrary, content: {
            ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
        })
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリからGoogleカレンダーにアクセスする

今回はiOSアプリからGoogle Calenear APIを通してGoogleカレンダーにアクセスしてみたいと思います。

環境: Xcode 12.0、Swift 5

準備

まずGoogle Cloud PlatformにアクセスしてGoogle Calendar APIを有効にします。
続いてOAuth認証を行うためのOAuthクライアントIDを取得します。
以下の記事の「クライアントIDを登録」に書かれている手順に従ってクライアントIDの取得の申請を行います。

Swift4でGoogleCalendarAPIを叩いてみた

使用する外部ライブラリ

Google API自体はREST APIになっていますが、これを直接呼び出すのは大変なので外部ライブラリを使用することにします。
今回は以下のライブラリを使用します。

・Google認証
AppAuth
GTMAppAuth

・Google Calendarとのアクセス
GoogleAPIClientForREST/Calendar

上記のライブラリはいずれもCocoaPodsで導入できます。
以下のようにPodfileを記述しpod installを実行して導入します。

platform :ios, '14.0'

target 'GoogleCalendarSample' do
  use_frameworks!

  pod 'AppAuth'
  pod 'GTMAppAuth'
  pod 'GoogleAPIClientForREST/Calendar'

end

認証

ではAppAuthとGTMAppAuthを使用してGoogle認証を行う処理を実装します。

まずAppDelegateにOIDExternalUserAgentSessionクラスを追加します。

AppDelegate.swift
import UIKit
import AppAuth
import GTMAppAuth

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var currentAuthorizationFlow: OIDExternalUserAgentSession?

---------------- (以下略) ----------------

その上でGoogle認証を行う画面で以下の処理を記述します。

import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
private let clientID = "xxxxxxxxxxxxxxxxxxxx"
private let reverseClientID = "xxxxxxxxxxxxxxxxxxxx"
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
    let scopes = ["https://www.googleapis.com/auth/calendar","https://www.googleapis.com/auth/calendar.readonly","https://www.googleapis.com/auth/calendar.events","https://www.googleapis.com/auth/calendar.events.readonly"]

    let configuration = GTMAppAuthFetcherAuthorization.configurationForGoogle()
    let redirectURL = URL.init(string: reverseClientID + ":/oauthredirect")

    let request = OIDAuthorizationRequest.init(configuration: configuration,
                                                   clientId: clientID,
                                                   scopes: scopes,
                                                   redirectURL: redirectURL!,
                                                   responseType: OIDResponseTypeCode,
                                                   additionalParameters: nil)

    let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
    appDelegate.currentAuthorizationFlow = OIDAuthState.authState(
        byPresenting: request,
        presenting: self,
        callback: { (authState, error) in
            if let error = error {
                NSLog("\(error)")
            } else {
                if let authState = authState {
                    self.authorization = GTMAppAuthFetcherAuthorization.init(authState: authState)
                    GTMAppAuthFetcherAuthorization.save(self.authorization!, toKeychainForName: "authorization")
                }
            }
            callBack(error)
    })
}

以下の変数にはOAuthクライアントIDを取得した時に入手したIDを記載します。
clientにはOAuth 2.0クライアントIDを、reverseClientIDにはリバースクライアントIDを記載します。

private let clientID = "xxxxxxxxxxxxxxxxxxxx"
private let reverseClientID = "xxxxxxxxxxxxxxxxxxxx"

今回必要とする権限を配列scopesに設定します。今回はGoogleカレンダーを検索・変更する為の権限を要求しています。

let scopes = ["https://www.googleapis.com/auth/calendar","https://www.googleapis.com/auth/calendar.readonly","https://www.googleapis.com/auth/calendar.events","https://www.googleapis.com/auth/calendar.events.readonly"]

OIDAuthStateクラスのauthStateメソッドが実行されたところで以下の様なGoogle認証のダイアログが表示されます。
Google認証ダイアログ
ユーザーがダイアログにgmailアドレスとパスワードを正しく入力し認証が完了した場合は、authStateメソッドのCallback関数でGTMAppAuthFetcherAuthorizationクラスを生成し保存します。
このGTMAppAuthFetcherAuthorizationクラスが残っているうちは認証ダイアログを再度表示する必要はありません。

イベントの検索

では続いてGoogleAPIClientForRESTを使用してGoogleカレンダーにアクセスしてみたいと思います。
まずはGoogleカレンダーから既存のイベントを取得する処理を記載します。
getメソッドに開始日時、終了日時を渡すとGoogleカレンダーから開始日時 〜 終了日時の間にあるイベントを検索するプログラムです。

import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
private let clientID = "xxxxxxxxxxxxxxxxxxxx"
private let reverseClientID = "xxxxxxxxxxxxxxxxxxxx"
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)
struct GoogleCalendaraEvent {
    var id: String
    var name: String
    var startDate: Date?
    var endDate: Date?
}
private var googleCalendarEventList: [GoogleCalendaraEvent] = []

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func get(startDateTime: Date, endDateTime: Date) {
    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.getCalendarEvents(startDateTime: startDateTime, endDateTime: endDateTime)
            }
        })
    } else {
        self.getCalendarEvents(startDateTime: startDateTime, endDateTime: endDateTime)
    }
}

private func getCalendarEvents(startDateTime: Date, endDateTime: Date) {
    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let query = GTLRCalendarQuery_EventsList.query(withCalendarId: "primary")
    query.timeMin = GTLRDateTime(date: startDateTime)
    query.timeMax = GTLRDateTime(date: endDateTime)

    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        } else {
            if let event = event as? GTLRCalendar_Events, let items = event.items {
                self.googleCalendarEventList.removeAll()
                for item in items {
                    let id: String = item.identifier ?? ""
                    let name: String = item.summary ?? ""
                    let startDate: Date? = item.start?.dateTime?.date
                    let endDate: Date? = item.end?.dateTime?.date
                    self.googleCalendarEventList.append(GoogleCalendaraEvent(id: id, name: name, startDate: startDate, endDate: endDate))
                }
            }
        }
    })
}

まずGoole認証が完了しているかの確認を行います。
GTMAppAuthFetcherAuthorizationクラスが保存されているかを確認し、保存されていなかった場合は先に作成したshowAuthorizationDialog関数を呼び出しGoogle認証のダイアログを表示してGTMAppAuthFetcherAuthorizationクラスを取得します。
GTMAppAuthFetcherAuthorizationクラスが保存されていた場合はそれをそのまま使用します。

続いてGoogleAPIClientForRESTを使用してGooglカレンダーからイベントを取得します。
まずGooleカレンダーにアクセスする為のGTLRCalendarServiceクラスを生成しauthorizerプロパティにGTMAppAuthFetcherAuthorizationクラスを設定します。

let calendarService = GTLRCalendarService()
calendarService.authorizer = self.authorization
calendarService.shouldFetchNextPages = true

続いてGoogleカレンダーからイベントを検索するためのGTLRCalendarQuery_EventsListクラスを生成し、検索条件として開始日時と終了日時を設定します。

let query = GTLRCalendarQuery_EventsList.query(withCalendarId: "primary")
query.timeMin = GTLRDateTime(date: startDateTime)
query.timeMax = GTLRDateTime(date: endDateTime)

そしてこのGTLRCalendarQuery_EventsListクラスを引数にしてGTLRCalendarServiceクラスのexecuteQueryメソッドを実行しGoogleカレンダーからイベントを取得します。
イベントを取得できた時はexecuteQueryメソッドのCallback関数でGTLRCalendar_Eventsクラスが返ってきますのでここからイベントの情報を取得します。

if let event = event as? GTLRCalendar_Events, let items = event.items {
    self.googleCalendarEventList.removeAll()
    for item in items {
        let id: String = item.identifier ?? ""
        let name: String = item.summary ?? ""
        let startDate: Date? = item.start?.dateTime?.date
        let endDate: Date? = item.end?.dateTime?.date
        self.googleCalendarEventList.append(GoogleCalendaraEvent(id: id, name: name, startDate: startDate, endDate: endDate))
    }
}

特にidentifier(イベントのユニークID)が重要です。
イベントの変更や削除を行う時はこのidentifierがキーになります。

イベントの追加

では次はGoogleカレンダーにイベントを追加してみたいと思います。
addメソッドにイベント名、開始日時、終了日時を渡すとGoogleカレンダーにイベントを作成するプログラムです。

import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func add(eventName: String, startDateTime: Date, endDateTime: Date) {

    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.addCalendarEvent(eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
            }
        })
    } else {
        self.addCalendarEvent(eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
    }
}

private func addCalendarEvent(eventName: String, startDateTime: Date, endDateTime: Date) {

    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let event = GTLRCalendar_Event()
    event.summary = eventName

    let gtlrDateTimeStart: GTLRDateTime = GTLRDateTime(date: startDateTime)
    let startEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    startEventDateTime.dateTime = gtlrDateTimeStart
    event.start = startEventDateTime

    let gtlrDateTimeEnd: GTLRDateTime = GTLRDateTime(date: endDateTime)
    let endEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    endEventDateTime.dateTime = gtlrDateTimeEnd
    event.end = endEventDateTime

    let query = GTLRCalendarQuery_EventsInsert.query(withObject: event, calendarId: "primary")
    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        }
    })
}

GTLRCalendarServiceクラスを生成するところまでは検索の場合と同じなのでその後の部分から説明します。
追加するイベントの情報を設定する為にGTLRCalendar_Eventクラスを生成します。
今回はイベントの名称、開始日時、終了日時を設定しますので、それぞれをGTLRCalendar_Eventクラスのsummaryプロパティ、startプロパティ、endプロパティに設定しています。

let event = GTLRCalendar_Event()
event.summary = eventName

let gtlrDateTimeStart: GTLRDateTime = GTLRDateTime(date: startDateTime)
let startEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
startEventDateTime.dateTime = gtlrDateTimeStart
event.start = startEventDateTime

let gtlrDateTimeEnd: GTLRDateTime = GTLRDateTime(date: endDateTime)
let endEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
endEventDateTime.dateTime = gtlrDateTimeEnd
event.end = endEventDateTime

尚、イベントのユニークIDとなるidentifierは新規追加の場合はGoogleカレンダーによって自動的に割り当てられますのでここで設定する必要はありません。

そしてGoogleカレンダーに新規追加する為のGTLRCalendarQuery_EventsInsertクラスをGTLRCalendar_Eventクラスを引数にして生成し、GTLRCalendarServiceクラスのexecuteQueryメソッドを実行することでGoogleカレンダーにイベントが新規追加されます。

イベントの変更

続いて既存のイベントの情報を変更してみます。
updateメソッドにイベントのidentifier、イベント名、開始日時、終了日時を渡すとGoogleカレンダーにある該当のidentifierのイベントの情報を変更するプログラムです。

import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func update(eventId: String, eventName: String, startDateTime: Date, endDateTime: Date) {

    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.updateCalendarEvent(eventId: eventId, eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
            }
        })
    } else {
        self.updateCalendarEvent(eventId: eventId, eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
    }
}

private func updateCalendarEvent(eventId: String, eventName: String, startDateTime: Date, endDateTime: Date) {
    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let event = GTLRCalendar_Event()
    event.identifier = eventId
    event.summary = eventName

    let gtlrDateTimeStart: GTLRDateTime = GTLRDateTime(date: startDateTime)
    let startEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    startEventDateTime.dateTime = gtlrDateTimeStart
    event.start = startEventDateTime

    let gtlrDateTimeEnd: GTLRDateTime = GTLRDateTime(date: endDateTime)
    let endEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    endEventDateTime.dateTime = gtlrDateTimeEnd
    event.end = endEventDateTime

    let query = GTLRCalendarQuery_EventsUpdate.query(withObject: event, calendarId: "primary", eventId: eventId)
    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        }
    })
}

更新の場合はGTLRCalendar_Eventクラスのidentifierプロパティに該当のイベントのIDを設定します。そして変更する値をGTLRCalendar_Eventクラスのプロパティに設定します。
その後、Googleカレンダーのイベントを更新する為のGTLRCalendarQuery_EventsUpdateクラスをGTLRCalendar_Eventクラスを引数にして生成し、それを引数にGTLRCalendarServiceクラスのexecuteQueryメソッドを実行します。

イベントの削除

最後にGoogleカレンダーのイベントの削除を行います。
deleteメソッドにイベントのidentifierを渡すとGoogleカレンダーから該当のイベントを削除するプログラムです。

import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func delete(eventId: String) {

    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.deleteCalendarEvent(eventId: eventId)
            }
        })
    } else {
        self.deleteCalendarEvent(eventId: eventId)
    }
}

private func deleteCalendarEvent(eventId: String) {
    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let query = GTLRCalendarQuery_EventsDelete.query(withCalendarId: "primary", eventId: eventId)
    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        }
    })
}

削除はGTLRCalendarQuery_EventsDeleteクラスをイベントのidentifierを引数にして生成し、それを引数にGTLRCalendarServiceのexecuteQueryメソッドを実行することで行うことができます。

サンプルプログラム

今回作成したサンプルプログラムはGitHubで公開しています。
https://github.com/naosekig/GoogleCalendarSample

参考文献

CocoaDocs.org - GoogleAPIClientForRest
Qiita:Swift4でGoogleCalendarAPIを叩いてみた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React NativeでのModalの作り方

はじめに

現在、React Nativeでアプリを開発しており、いい感じのModal(ボタンを押したら下から画面が出てくるやつ)を作ろうとしたのですが、悪戦苦闘したため、記事に残し備忘録の1つとして残します。

作り方

まずはModal画面に遷移するボタンを作成しましょう。ちなみに、今回はuseStateを使用します。

App.js
export default function App() {
  const [modalOpen, setModalOpen] = useState(false);

  return (
    <View>
      <View>
        <Text style={{ fontSize: 24 }}>Lists</Text>

        <TouchableHighlight
          style={styles.openButton}
          onPress={() => {
            setModalOpen(true);
          }}
        >
          <Text>Add</Text>
        </TouchableHighlight>
      </View>
  );
}

// スタイル

const styles = StyleSheet.create({
  openButton: {
    backgroundColor: "#F194FF",
    borderRadius: 20,
    padding: 10,
    paddingLeft: 20,
    paddingRight: 20,
    elevation: 2,
  },
});

Buttonタグでもいいのですが、ボタンらしいスタイルにしたいので今回はTouchableHighlightを使用しました。以下のスタイルのようになります。
スクリーンショット 2020-09-27 9.09.17.png

Modalを追加

App.js
return (
    <View>

{/* 投稿画面 */}
      <Modal visible={modalOpen} animationType="slide">
        <View style={StyleSheet.modelContent}>
          <Text
            style={{
              fontSize: 30,
              paddingTop: 50,
              marginLeft: "auto",
              marginRight: "auto",
              marginTop: "auto",
              marginBottom: 70,
            }}
          >
            Modal画面
          </Text>
        </View>
      </Modal>

    <View> ~ </View>

    </View>
);

先ほどのコードの上にModalを追加すれば、完成です。Modal画面とボタンがある画面は別のファイルにコード記載した方が良さそうですね。今回は説明のためにあえて一緒に記述させました。以下がModal画面です。スクリーンショット 2020-09-27 5.50.43.png
これが下から出てきます。

あとがき

本当はMaterial Iconを押した時に、Modalが出てくるようにしたいのですが、調べてもよくわからなかったので、自作でボタンを作成して実装しました。分かる方がいましたら、教えて欲しいです。

参考

React Native - Modal

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Visionで画像の位置合わせ

パノラマ画像や手ブレ補正に使えます

IMG_1399.JPG  IMG_1400.JPG
名称未設定.png

手順

2枚目の画像で登録リクエストを作成します。

let request = VNTranslationalImageRegistrationRequest(targetedCIImage: image2, options: [:])

1枚目の画像で実行します。

let handler = VNImageRequestHandler(ciImage: image1, options: [:])
do {
    try handler.perform([request])
   } catch let error {
    print(error)
   }

結果は2枚目の画像を1枚目の画像に位置合わせするためのCGAffineTransformで返されます。

guard let observation = request.results?.first as? VNImageTranslationAlignmentObservation else { return }
let alignmentTransform = observation.alignmentTransform
print(alignmentTransform)
// CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: -777.0, ty: 74.0)

結果を元に、2枚の画像を合成します。

image2 = image2.transformed(by: alignmentTransform)
let compositedImage = image1.applyingFilter("CIAdditionCompositing", parameters: ["inputBackgroundImage": image2])

注意点

・何回か試してみたところ、画像内の顕著なオブジェクトを基準に合わせる傾向がある気がします。
・カメラで撮った画像の向きに注意しましょう。90度回転していたりします。
・水平移動には強いですが、カメラが回転すると、結果が変にずれやすいです。
上記のせいで不安定な挙動をすることがあります。

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む