20220131のiOSに関する記事は10件です。

50行で目線トラッキング(iPhone)

はじめに こちらの記事を見て目線トラッキングが面白そうだなと思いました。 目線でブラウザをスクロールするアプリを作ってみた 調べてみると 50 行くらいで下記ができました(動作確認は iOS 15.1 の iPhone 12 mini)。 目線トラッキングができてるようなできてないような?コード量50行! pic.twitter.com/6uiApUJnPn— am10 (@am103141592) January 28, 2022 実装 とりあえずソース。 import UIKit import ARKit final class ViewController: UIViewController { private let session = ARSession() private var lookAtPointView: UIImageView = { let image = UIImageView(image: .init(systemName: "eye")) image.frame = .init(origin: .zero, size: CGSize(width: 30, height: 30)) image.contentMode = .scaleAspectFit return image }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(lookAtPointView) session.delegate = self } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let configuration = ARFaceTrackingConfiguration() configuration.isLightEstimationEnabled = true session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) } } extension ViewController: ARSessionDelegate { func session(_ session: ARSession, didUpdate frame: ARFrame) { guard let faceAnchor = frame.anchors.first(where: { $0 is ARFaceAnchor }) as? ARFaceAnchor else { return } let lookingPoint = frame.camera.projectPoint(faceAnchor.lookAtPoint, orientation: .portrait, viewportSize: view.bounds.size) DispatchQueue.main.async { self.lookAtPointView.center = lookingPoint } } } これだけで目線トラッキングぽいことができます 必要条件 ARKit の ARFaceTrackingConfiguration を利用するので TrueDepth カメラを搭載した端末(iPhone X 以降?)が必要です。 ドキュメント:ARFaceTrackingConfiguration やりたかったこと ピアノ演奏中の楽譜への目線とか本読むのが速い人の目線の動きとかを iPhone で見れたらおもしろいと思った。 が、厳密に目線を取得はできなかったのであきらめました。。。(どちらかというと顔の向きの取得?) ドキュメント:ARFaceAnchor つくったもの 上記のものは作れなさそうでしたが目線トラッキングはおもしろいので何か他のものを作れないかと思い色々考えてみました。 調べてみると ARFaceAnchor.BlendShapeLocation で左右の瞬きなどが取れそうです。 ドキュメント:ARFaceAnchor.BlendShapeLocation 1つ目 瞬きで撃つシューティングゲーム(音声あり)。 やりたいことできなそうだったから路線変更して瞬きで撃つシューティングつくった? pic.twitter.com/nDSIIEMBz3— am10 (@am103141592) January 29, 2022 2つ目 目線をマウスカーソル瞬きをクリックに対応させた PC のような操作ができるなにか。 目線をマウスカーソルまばたきをクリックに対応させることでiPhoneでPCのような操作を実現? pic.twitter.com/9KFblAJ5bp— am10 (@am103141592) January 30, 2022 おわりに 目線トラッキングおもしろそうなのでなにか使えそうなのですが私の発想ではなかなかむずかしかったです。 なにかに使えそう 参考 目線でブラウザをスクロールするアプリを作ってみた ドキュメント:ARFaceTrackingConfiguration ドキュメント:ARFaceAnchor
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリの検索ビューにおけるタグの追加

iOSの検索ビュー内でタグを追加し、検索対象のカテゴリーを指定することができます。 このタグは検索トークンと呼ばれるものです。 ここでは、検索トークンを作成し、それを検索バーに追加する方法を説明します。 さらに、SwiftUIアプリに検索トークンを使った検索バーを加えることについても説明します。 サーチトークンの作成 UISearchTokenプロパティを使用し、サーチトークンを作成することができます。このオブジェクトは、UIImageアイコンやタイトルで初期化することができます。 Apple Developer Documentation また、トークンの識別子を指定する必要があります(representedObjectプロパティを使用)。この変数はどのような型でもよく、コードで自分が参照するために使います。一般的に、この変数には文字列の識別子を設定します。 例えば、ユーザーがSwiftUIについての記事を検索している場合、次のようにUISearchTokenオブジェクトを初期化することができます: var token = UISearchToken(icon: UIImage(systemName: "swift"), text: "SwiftUI") token.representedObject = "#swiftui" 識別子は検索トークンから後で読み取ることができます。 if let identifier = token.representedObject as? String { print(identifier) } 検索トークンを検索バーに追加する UISearchControllerでは、検索トークンを設定できます。 searchController.searchBar.searchTextField.tokens = searchTokens また、検索トークンを追加または削除したり、テキスト内にあるトークンの位置を取得したり、指定されたテキスト位置の間にある全トークンを取得したりするなどの追加機能もあります。 open var tokens: [UISearchToken] open func insertToken(_ token: UISearchToken, at tokenIndex: Int) open func removeToken(at tokenIndex: Int) open func positionOfToken(at tokenIndex: Int) -> UITextPosition open func tokens(in textRange: UITextRange) -> [UISearchToken] UIKitアプリに検索トークンを追加する UIKitアプリに検索トークンを追加するのは簡単です。UISearchControllerを作成し、searchController.searchBar.searchTextField.tokens変数を使用して検索トークンをそのビューコントローラに割り当てるだけです。 SwiftUIアプリに検索トークンを追加する SwiftUIには、新たな.searchableビューモディファイアがあります。しかし、このモディファイアは検索トークンの追加をサポートしていません。そのため、UIViewControllerRepresentable構造を実装する必要があります。 UIKitコンポーネントをSwiftUIのナビゲーションバーに追加する 検索バー(UIKitコンポーネント)を単に表示するだけではなく、SwiftUIのナビゲーションバーに直接追加したいと思います。 class NavBarEmbeddedSearch: UIViewController { let searchController = UISearchController() override func viewDidLoad() { searchController.hidesNavigationBarDuringPresentation = true searchController.obscuresBackgroundDuringPresentation = false } override func viewWillAppear(_ animated: Bool) { self.parent?.navigationItem.searchController = searchController } } viewDidLoad関数内では、検索バーのUIに関する基本的な設定を行います。 このビューコントローラが画面に表示されるとき(viewWillAppearが呼び出されるとき)、検索コントローラが親ナビゲーションバー(SwiftUIが管理するもの)に割り当てられます。 SearchBar(サーチバー)構造体の作成 次に、SearchBarの基本構造を作成します。これはタイプUIViewControllerRepresentableに準拠するもので、UIKitをSwiftUIに移植する(利用できるようにする)ことを意味します。 fileprivate struct SearchBar: UIViewControllerRepresentable { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func makeUIViewController(context: Context) -> NavBarEmbeddedSearch { // TODO 1 } func updateUIViewController(_ controller: NavBarEmbeddedSearch, context: Context) { } func makeCoordinator() -> Coordinator { return Coordinator(searchText: $searchText, searchTokens: $searchTokens) } class Coordinator: NSObject, UISearchResultsUpdating { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func updateSearchResults(for searchController: UISearchController) { // TODO 2 } } } 上記のコードでご覧のように、SearchBar構造体には検索テキストのバインディング変数と検索トークンが格納されることになります。 また、デリゲートとして動作するCoordinatorクラスもあり、検索バーから更新情報を受け取ります(ユーザーが検索テキストを入力したときやユーザーが検索トークンを削除したときなど)。 検索バーの初期化 上記のコードの// TODO 1で、検索コントローラーの初期化を行います。 func makeUIViewController(context: Context) -> NavBarEmbeddedSearch { let controller = NavBarEmbeddedSearch() controller.searchController.searchBar.searchTextField.tokens = searchTokens controller.searchController.searchResultsUpdater = context.coordinator return controller } ここでは、サーチトークンに提供された値を設定します。 また、検索結果アップデータsearchResultsUpdater変数には、作成したCoordinatorを設定します。これにより、検索文字が変更されたことがわかるようになります。 検索文字の変更でSwiftUIのバインディング変数を更新する 検索文字に変更があった場合、Coordinatorオブジェクト内のfunc updateSearchResults(for searchController: UISearchController)関数が呼び出されます。 上記のコードの// TODO 2コードブロックの中で、更新された検索文字でSwiftUIのバインディング変数を更新します。 guard let text = searchController.searchBar.text else { return } self.searchText = text self.searchTokens = searchController.searchBar.searchTextField.tokens 完成したコード こちらが、完成したSearchBarのコードです class NavBarEmbeddedSearch: UIViewController { let searchController = UISearchController() override func viewDidLoad() { searchController.hidesNavigationBarDuringPresentation = true searchController.obscuresBackgroundDuringPresentation = false } override func viewWillAppear(_ animated: Bool) { self.parent?.navigationItem.searchController = searchController } } fileprivate struct SearchBar: UIViewControllerRepresentable { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func makeUIViewController(context: Context) -> NavBarEmbeddedSearch { let controller = NavBarEmbeddedSearch() controller.searchController.searchResultsUpdater = context.coordinator controller.searchController.searchBar.searchTextField.tokens = searchTokens return controller } func updateUIViewController(_ controller: NavBarEmbeddedSearch, context: Context) { } func makeCoordinator() -> Coordinator { return Coordinator(searchText: $searchText, searchTokens: $searchTokens) } class Coordinator: NSObject, UISearchResultsUpdating { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func updateSearchResults(for searchController: UISearchController) { guard let text = searchController.searchBar.text else { return } self.searchText = text self.searchTokens = searchController.searchBar.searchTextField.tokens } } } SwiftUIビュー内に検索バーを設置する ここでは、SwiftUIビューのナビゲーションバーに検索バーを追加します。 まずは、SwiftUIビューがナビゲーションビュー内に埋め込まれていることを確認します。 検索コントローラーは、ナビゲーションバーにプログラムで直接割り当てるため、検索バーをビューコンポーネントにする必要はありません。従って、overlayビューモディファイアを利用することで、そのフレームを0に設定します。 struct ContentView: View { /// Variables used by `SearchView` @State private var searchText: String = "" @State private var searchTokens: [UISearchToken] init(searchTokens: [UISearchToken]) { self._searchTokens = .init(initialValue: searchTokens) } var body: some View { Form { Section { Text("Search term: \(searchText)") Text("Search tokens: \(searchTokens.getTokenNames().description)") } }.overlay( SearchBar(searchText: $searchText, searchTokens: $searchTokens).frame(width: 0, height: 0) ) } } 上の例では、入力された検索テキストとトークンを表示しているだけです。ご自分のアプリケーションでは、検索ワードが変わるたびに検索を実行する必要があります。 上記のSwiftUIビューを使う これで、上記のSwiftUIビューを使って、タグと共に検索バーを表示できるようになりました。 struct MenuView: View { var body: some View { List { NavigationLink("Movies") { ContentView(searchTokens: [.init(icon: UIImage(systemName: "play.rectangle.on.rectangle.fill"), text: "Movies").getTokenWithIdentifier("movies")]) } NavigationLink("Music") { ContentView(searchTokens: [.init(icon: UIImage(systemName: "music.note"), text: "Music").getTokenWithIdentifier("music")]) } NavigationLink("Documents") { ContentView(searchTokens: [.init(icon: UIImage(systemName: "doc"), text: "Documents").getTokenWithIdentifier("documents")]) } } .navigationTitle("Search demo") .navigationBarTitleDisplayMode(.inline) } } 上の例では、ユーザーは、映画、歌、文書のいずれかの検索を選択できます。ユーザーが検索しているカテゴリーは、タグ(検索トークン)として表示されます。 完成したプロジェクト 完成したプロジェクトのコードをこちらからご覧いただけます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリの検索ビューにおけるタグ(UISearchToken)の追加

iOSの検索ビュー内でタグを追加し、検索対象のカテゴリーを指定することができます。 このタグは検索トークンと呼ばれるものです。 ここでは、検索トークンを作成し、それを検索バーに追加する方法を説明します。 さらに、SwiftUIアプリに検索トークンを使った検索バーを加えることについても説明します。 サーチトークンの作成 UISearchTokenプロパティを使用し、サーチトークンを作成することができます。このオブジェクトは、UIImageアイコンやタイトルで初期化することができます。 Apple Developer Documentation また、トークンの識別子を指定する必要があります(representedObjectプロパティを使用)。この変数はどのような型でもよく、コードで自分が参照するために使います。一般的に、この変数には文字列の識別子を設定します。 例えば、ユーザーがSwiftUIについての記事を検索している場合、次のようにUISearchTokenオブジェクトを初期化することができます: var token = UISearchToken(icon: UIImage(systemName: "swift"), text: "SwiftUI") token.representedObject = "#swiftui" 識別子は検索トークンから後で読み取ることができます。 if let identifier = token.representedObject as? String { print(identifier) } 検索トークンを検索バーに追加する UISearchControllerでは、検索トークンを設定できます。 searchController.searchBar.searchTextField.tokens = searchTokens また、検索トークンを追加または削除したり、テキスト内にあるトークンの位置を取得したり、指定されたテキスト位置の間にある全トークンを取得したりするなどの追加機能もあります。 open var tokens: [UISearchToken] open func insertToken(_ token: UISearchToken, at tokenIndex: Int) open func removeToken(at tokenIndex: Int) open func positionOfToken(at tokenIndex: Int) -> UITextPosition open func tokens(in textRange: UITextRange) -> [UISearchToken] UIKitアプリに検索トークンを追加する UIKitアプリに検索トークンを追加するのは簡単です。UISearchControllerを作成し、searchController.searchBar.searchTextField.tokens変数を使用して検索トークンをそのビューコントローラに割り当てるだけです。 SwiftUIアプリに検索トークンを追加する SwiftUIには、新たな.searchableビューモディファイアがあります。しかし、このモディファイアは検索トークンの追加をサポートしていません。そのため、UIViewControllerRepresentable構造を実装する必要があります。 UIKitコンポーネントをSwiftUIのナビゲーションバーに追加する 検索バー(UIKitコンポーネント)を単に表示するだけではなく、SwiftUIのナビゲーションバーに直接追加したいと思います。 class NavBarEmbeddedSearch: UIViewController { let searchController = UISearchController() override func viewDidLoad() { searchController.hidesNavigationBarDuringPresentation = true searchController.obscuresBackgroundDuringPresentation = false } override func viewWillAppear(_ animated: Bool) { self.parent?.navigationItem.searchController = searchController } } viewDidLoad関数内では、検索バーのUIに関する基本的な設定を行います。 このビューコントローラが画面に表示されるとき(viewWillAppearが呼び出されるとき)、検索コントローラが親ナビゲーションバー(SwiftUIが管理するもの)に割り当てられます。 SearchBar(サーチバー)構造体の作成 次に、SearchBarの基本構造を作成します。これはタイプUIViewControllerRepresentableに準拠するもので、UIKitをSwiftUIに移植する(利用できるようにする)ことを意味します。 fileprivate struct SearchBar: UIViewControllerRepresentable { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func makeUIViewController(context: Context) -> NavBarEmbeddedSearch { // TODO 1 } func updateUIViewController(_ controller: NavBarEmbeddedSearch, context: Context) { } func makeCoordinator() -> Coordinator { return Coordinator(searchText: $searchText, searchTokens: $searchTokens) } class Coordinator: NSObject, UISearchResultsUpdating { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func updateSearchResults(for searchController: UISearchController) { // TODO 2 } } } 上記のコードでご覧のように、SearchBar構造体には検索テキストのバインディング変数と検索トークンが格納されることになります。 また、デリゲートとして動作するCoordinatorクラスもあり、検索バーから更新情報を受け取ります(ユーザーが検索テキストを入力したときやユーザーが検索トークンを削除したときなど)。 検索バーの初期化 上記のコードの// TODO 1で、検索コントローラーの初期化を行います。 func makeUIViewController(context: Context) -> NavBarEmbeddedSearch { let controller = NavBarEmbeddedSearch() controller.searchController.searchBar.searchTextField.tokens = searchTokens controller.searchController.searchResultsUpdater = context.coordinator return controller } ここでは、サーチトークンに提供された値を設定します。 また、検索結果アップデータsearchResultsUpdater変数には、作成したCoordinatorを設定します。これにより、検索文字が変更されたことがわかるようになります。 検索文字の変更でSwiftUIのバインディング変数を更新する 検索文字に変更があった場合、Coordinatorオブジェクト内のfunc updateSearchResults(for searchController: UISearchController)関数が呼び出されます。 上記のコードの// TODO 2コードブロックの中で、更新された検索文字でSwiftUIのバインディング変数を更新します。 guard let text = searchController.searchBar.text else { return } self.searchText = text self.searchTokens = searchController.searchBar.searchTextField.tokens 完成したコード こちらが、完成したSearchBarのコードです class NavBarEmbeddedSearch: UIViewController { let searchController = UISearchController() override func viewDidLoad() { searchController.hidesNavigationBarDuringPresentation = true searchController.obscuresBackgroundDuringPresentation = false } override func viewWillAppear(_ animated: Bool) { self.parent?.navigationItem.searchController = searchController } } fileprivate struct SearchBar: UIViewControllerRepresentable { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func makeUIViewController(context: Context) -> NavBarEmbeddedSearch { let controller = NavBarEmbeddedSearch() controller.searchController.searchResultsUpdater = context.coordinator controller.searchController.searchBar.searchTextField.tokens = searchTokens return controller } func updateUIViewController(_ controller: NavBarEmbeddedSearch, context: Context) { } func makeCoordinator() -> Coordinator { return Coordinator(searchText: $searchText, searchTokens: $searchTokens) } class Coordinator: NSObject, UISearchResultsUpdating { @Binding var searchText: String @Binding var searchTokens: [UISearchToken] init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) { self._searchText = searchText self._searchTokens = searchTokens } func updateSearchResults(for searchController: UISearchController) { guard let text = searchController.searchBar.text else { return } self.searchText = text self.searchTokens = searchController.searchBar.searchTextField.tokens } } } SwiftUIビュー内に検索バーを設置する ここでは、SwiftUIビューのナビゲーションバーに検索バーを追加します。 まずは、SwiftUIビューがナビゲーションビュー内に埋め込まれていることを確認します。 検索コントローラーは、ナビゲーションバーにプログラムで直接割り当てるため、検索バーをビューコンポーネントにする必要はありません。従って、overlayビューモディファイアを利用することで、そのフレームを0に設定します。 struct ContentView: View { /// Variables used by `SearchView` @State private var searchText: String = "" @State private var searchTokens: [UISearchToken] init(searchTokens: [UISearchToken]) { self._searchTokens = .init(initialValue: searchTokens) } var body: some View { Form { Section { Text("Search term: \(searchText)") Text("Search tokens: \(searchTokens.getTokenNames().description)") } }.overlay( SearchBar(searchText: $searchText, searchTokens: $searchTokens).frame(width: 0, height: 0) ) } } 上の例では、入力された検索テキストとトークンを表示しているだけです。ご自分のアプリケーションでは、検索ワードが変わるたびに検索を実行する必要があります。 上記のSwiftUIビューを使う これで、上記のSwiftUIビューを使って、タグと共に検索バーを表示できるようになりました。 struct MenuView: View { var body: some View { List { NavigationLink("Movies") { ContentView(searchTokens: [.init(icon: UIImage(systemName: "play.rectangle.on.rectangle.fill"), text: "Movies").getTokenWithIdentifier("movies")]) } NavigationLink("Music") { ContentView(searchTokens: [.init(icon: UIImage(systemName: "music.note"), text: "Music").getTokenWithIdentifier("music")]) } NavigationLink("Documents") { ContentView(searchTokens: [.init(icon: UIImage(systemName: "doc"), text: "Documents").getTokenWithIdentifier("documents")]) } } .navigationTitle("Search demo") .navigationBarTitleDisplayMode(.inline) } } 上の例では、ユーザーは、映画、歌、文書のいずれかの検索を選択できます。ユーザーが検索しているカテゴリーは、タグ(検索トークン)として表示されます。 完成したプロジェクト 完成したプロジェクトのコードをこちらからご覧いただけます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITextViewに入力されている文字数をカウントする

TextViewに入力されている文字数のリアルタイムカウントを実装します。 文字列中の全ての空白や改行はカウントしません。 環境 Xcode 13.2.1 Swift 5.5.2 手順 StringProtocolを拡張します。 Extension.swift extension StringProtocol where Self: RangeReplaceableCollection { var removeWhitespacesAndNewlines: Self { filter { !$0.isNewline && !$0.isWhitespace} } } 使用例 ViewController.swift extension ViewController: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { let textLength = textView.text.removeWhitespacesAndNewlines.count navigationTitle.text = "total: \(textLength)" } } 終わりに   終わりです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WidgetKitで天気予報アプリ作ってみた〜天気情報取得編〜

投稿の経緯 最近仕事でウィジェットをよく触っており、復習とアウトプットを兼ねて天気予報アプリを開発しているので記事にしました。 今回使う天気情報はOpenWeatherMapのOneCallAPIから取得します。 開発環境 Swift 5.5 Xcode 13.2.1 サンプルプロジェクト GitHubにPushしています。気になる方はご覧ください。 https://github.com/ken-sasaki-222/WeatherWidget APIを叩いてみる { "lat": 33.44, "lon": -94.04, "timezone": "America/Chicago", "timezone_offset": -21600, "current": { "dt": 1618317040, "sunrise": 1618282134, "sunset": 1618333901, "temp": 284.07, "feels_like": 282.84, "pressure": 1019, "humidity": 62, "dew_point": 277.08, "uvi": 0.89, "clouds": 0, "visibility": 10000, "wind_speed": 6, "wind_deg": 300, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" } ], "rain": { "1h": 0.21 } }, "minutely": [ { "dt": 1618317060, "precipitation": 0.205 }, ... }, "hourly": [ { "dt": 1618315200, "temp": 282.58, "feels_like": 280.4, "pressure": 1019, "humidity": 68, "dew_point": 276.98, "uvi": 1.4, "clouds": 19, "visibility": 306, "wind_speed": 4.12, "wind_deg": 296, "wind_gust": 7.33, "weather": [ { "id": 801, "main": "Clouds", "description": "few clouds", "icon": "02d" } ], "pop": 0 }, ... } "daily": [ { "dt": 1618308000, "sunrise": 1618282134, "sunset": 1618333901, "moonrise": 1618284960, "moonset": 1618339740, "moon_phase": 0.04, "temp": { "day": 279.79, "min": 275.09, "max": 284.07, "night": 275.09, "eve": 279.21, "morn": 278.49 }, "feels_like": { "day": 277.59, "night": 276.27, "eve": 276.49, "morn": 276.27 }, "pressure": 1020, "humidity": 81, "dew_point": 276.77, "wind_speed": 3.06, "wind_deg": 294, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" } ], "clouds": 56, "pop": 0.2, "rain": 0.62, "uvi": 1.93 }, ... }, "alerts": [ { "sender_name": "NWS Tulsa", "event": "Heat Advisory", "start": 1597341600, "end": 1597366800, "description": "...HEAT ADVISORY REMAINS IN EFFECT FROM 1 PM THIS AFTERNOON TO\n8 PM CDT THIS EVENING...\n* WHAT...Heat index values of 105 to 109 degrees expected.\n* WHERE...Creek, Okfuskee, Okmulgee, McIntosh, Pittsburg,\nLatimer, Pushmataha, and Choctaw Counties.\n* WHEN...From 1 PM to 8 PM CDT Thursday.\n* IMPACTS...The combination of hot temperatures and high\nhumidity will combine to create a dangerous situation in which\nheat illnesses are possible.", "tags": [ "Extreme temperature value" ] }, ... ] 公式のレスポンス例を参考にしていますが、このままでは取得する情報が多く、使わないものもあるので条件を絞ってリクエストURLを作成したいと思います。条件は以下の通り。 hourly以外は取得しない 温度は摂氏で取得 言語は日本対応で取得 hourly以外は取得しない パラメータのexcludeにcurrent minutely daily alertsを指定しhourly以外は取得しないように設定します。 カンマ区切りのスペースなしである必要があります。 温度は摂氏で取得 温度を摂氏で取得するにはパラメータunitsにmetricを指定します。 言語は日本対応で取得 パラメータlangにjaを指定します。 リクエストURLの確認 https://api.openweathermap.org/data/2.5/onecall?lat=35.65146&lon=139.63678&units=metric&exclude=current,minutely,daily,alerts&lang=ja&appid={キー} 作成したリクエストURLです。緯度経度は東京都のとある地点を使っています。(将来的に現在地から緯度経度を取得します) レスポンスをもとにModelを作成 { "lat": 35.6515, "lon": 139.6368, "timezone": "Asia/Tokyo", "timezone_offset": 32400, "hourly": [ { "dt": 1643446800, "temp": 7.16, "feels_like": 4.87, "pressure": 1009, "humidity": 50, "dew_point": -2.26, "uvi": 0, "clouds": 75, "visibility": 8073, "wind_speed": 3.37, "wind_deg": 102, "wind_gust": 5.11, "weather": [ { "id": 500, "main": "Rain", "description": "小雨", "icon": "10n" } ], "pop": 0.21, "rain": { "1h": 0.12 } }, .... } 作成したリクエストを送るとこのようにレスポンスが返ってきます。 今回はhourlyから現在時刻〜48時間分のdt、temp、pressureと、weatherのmainのみを取得します。 WeatherResponseModel.swift import Foundation struct WeatherResponseModel: Decodable { var hourly: [Hourly] } struct Hourly: Decodable { var dt: Double var temp: Double var pressure: Double var weather: [Weather] } struct Weather: Decodable { var main: String } リクエストModelを作成 WeatherRequestModel.swift import Foundation struct WeatherRequestModel { var lat: Double var lng: Double } 緯度経度を格納するリクエストモデルです。 実装 アーキテクチャ MVVM + リポジトリパターンを採用して今回は開発しています。 DataStore WeatherDataStore.swift import Foundation final class WeatherDataStore { private let baseUrl = "https://api.openweathermap.org/data/2.5/onecall" private let shared = URLSession.shared private let decoder = JSONDecoder() func fetchWeathers(requestModel: WeatherRequestModel) async throws -> WeatherResponseModel { let params = "lat=\(requestModel.lat)&lon=\(requestModel.lng)&units=metric&exclude=current,minutely,daily,alerts&lang=ja&appid=\(API_KEY)" let urlString = baseUrl + "?" + params guard let url = URL(string: urlString) else { throw NSError(domain: "Error fetch weathers.", code: -1) } let request = URLRequest(url: url) let (data, _) = try await shared.data(for: request) let response = try decoder.decode(WeatherResponseModel.self, from: data) return response } } APIと通信するDataStoreです。今回async/awaitを使って非同期処理を書いています。 WeatherWidgetTests.swift import XCTest class WeatherWidgetTests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testWeatherDataStore() async throws { let dataStore = WeatherDataStore() let request = WeatherRequestModel(lat: 35.65146, lng: 139.63678) let response = try await dataStore.fetchWeathers(requestModel: request) print("Success fetch weathers.") print("Response hourly:", response.hourly) XCTAssert(response.hourly.count > 0) } } データに依存するので完全なテストではないですが、動作確認レベルでDataStoreのテストを書きました。このテストでAPI通信の挙動を確認できるので、テストが成功して正常に値が取得できているのであればData層の開発は完了です。 Repository WeatherRepositoryInterface.swift import Foundation protocol WeatherRepositoryInterface { func fetchWeathers(requestModel: WeatherRequestModel) async throws -> WeatherResponseModel } WeatherRepository.swift import Foundation class WeatherRepository: WeatherRepositoryInterface { private let weatherDataStore = WeatherDataStore() func fetchWeathers(requestModel: WeatherRequestModel) async throws -> WeatherResponseModel { do { let response = try await weatherDataStore.fetchWeathers(requestModel: requestModel) return response } catch { throw error } } } WeatherDataStoreへrequestModelを渡してdo catchで値を受け取ります。受け取った結果はasync throwsでViewModelに返します。後々開発するWidget ExtensionからもこのRepositoryを経由して天気情報を取得します。 ViewModel RepositoryRocator.swift import Foundation class RepositoryRocator { static func getWeatherRepository() -> WeatherRepositoryInterface { WeatherRepository() } } WeatherViewModel.swift import Foundation class WeatherViewModel: NSObject { private let weatherRepository: WeatherRepositoryInterface init(weatherRepository: WeatherRepositoryInterface) { self.weatherRepository = weatherRepository super.init() } override convenience init() { self.init(weatherRepository: RepositoryRocator.getWeatherRepository()) } func createRequestModel() -> WeatherRequestModel { let requestModel = WeatherRequestModel( lat: 35.65146, lng: 139.63678 ) return requestModel } func fetchWeathers() async { do { let response = try await weatherRepository.fetchWeathers(requestModel: createRequestModel()) print("Success fetch weathers:", response.hourly) } catch { print("Error fetch weathers:", error) } } } WeatherRepositoryへアクセスするViewModelです。RepositoryはRepositoryRocatorを経由してRepositoryInterface(抽象)から取得しています。 View ContentView.swift import SwiftUI struct ContentView: View { private let weatherVM = WeatherViewModel() var body: some View { Button { Task { await weatherVM.fetchWeathers() } } label: { Text("天気情報取得") .font(.system(size: 18, weight: .medium, design: .default)) .padding(.horizontal, 80) .padding(.vertical, 12) .foregroundColor(.white) .background(.orange) .cornerRadius(100) } } } 今回はウィジェットの開発がメインなのでアプリのViewはボタンタップで天気情報の取得だけにしておきます。 レスポンスのログ確認 Success fetch weathers: [ WeatherWidget.Hourly(dt: 1643612400.0, temp: 8.05, pressure: 1013.0, weather: [WeatherWidget.Weather(main: "Clouds")]), WeatherWidget.Hourly(dt: 1643616000.0, temp: 8.21, pressure: 1013.0, weather: [WeatherWidget.Weather(main: "Clouds")]), WeatherWidget.Hourly(dt: 1643619600.0, temp: 7.85, pressure: 1014.0, weather: [WeatherWidget.Weather(main: "Clouds")]), ... ] 48時間分の時間、気温、気圧、weatherが取得できているので取得成功です。 おわりに 今回はWidgetKitで天気予報アプリ作ってみた〜天気情報取得編〜について書きました。 現状、緯度経度を直接指定しているので、WidgetKitで天気予報アプリ作ってみた〜位置情報取得&保存編〜で書き換えていきます。 続きが気になる方は↓こちら↓から ご覧いただきありがとうございました。 こうしたほうがいいや、ここはちょっと違うなど気になる箇所があった場合、ご教示いただけると幸いです。 お知らせ 現在副業でiOSアプリ開発案件を募集しています。 Twitter DMでご依頼お待ちしております! ↓活動リンクはこちら↓ https://linktr.ee/sasaki.ken
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WidgetKitで天気予報アプリ作ってみた①〜天気情報取得編〜

投稿の経緯 最近仕事でウィジェットをよく触っており、復習とアウトプットを兼ねて天気予報アプリを開発しているので記事にしました。 今回使う天気情報はOpenWeatherMapのOneCallAPIから取得します。 開発環境 Swift 5.5 Xcode 13.2.1 サンプルプロジェクト GitHubにPushしています。気になる方はご覧ください。 https://github.com/ken-sasaki-222/WeatherWidget APIを叩いてみる { "lat": 33.44, "lon": -94.04, "timezone": "America/Chicago", "timezone_offset": -21600, "current": { "dt": 1618317040, "sunrise": 1618282134, "sunset": 1618333901, "temp": 284.07, "feels_like": 282.84, "pressure": 1019, "humidity": 62, "dew_point": 277.08, "uvi": 0.89, "clouds": 0, "visibility": 10000, "wind_speed": 6, "wind_deg": 300, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" } ], "rain": { "1h": 0.21 } }, "minutely": [ { "dt": 1618317060, "precipitation": 0.205 }, ... }, "hourly": [ { "dt": 1618315200, "temp": 282.58, "feels_like": 280.4, "pressure": 1019, "humidity": 68, "dew_point": 276.98, "uvi": 1.4, "clouds": 19, "visibility": 306, "wind_speed": 4.12, "wind_deg": 296, "wind_gust": 7.33, "weather": [ { "id": 801, "main": "Clouds", "description": "few clouds", "icon": "02d" } ], "pop": 0 }, ... } "daily": [ { "dt": 1618308000, "sunrise": 1618282134, "sunset": 1618333901, "moonrise": 1618284960, "moonset": 1618339740, "moon_phase": 0.04, "temp": { "day": 279.79, "min": 275.09, "max": 284.07, "night": 275.09, "eve": 279.21, "morn": 278.49 }, "feels_like": { "day": 277.59, "night": 276.27, "eve": 276.49, "morn": 276.27 }, "pressure": 1020, "humidity": 81, "dew_point": 276.77, "wind_speed": 3.06, "wind_deg": 294, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" } ], "clouds": 56, "pop": 0.2, "rain": 0.62, "uvi": 1.93 }, ... }, "alerts": [ { "sender_name": "NWS Tulsa", "event": "Heat Advisory", "start": 1597341600, "end": 1597366800, "description": "...HEAT ADVISORY REMAINS IN EFFECT FROM 1 PM THIS AFTERNOON TO\n8 PM CDT THIS EVENING...\n* WHAT...Heat index values of 105 to 109 degrees expected.\n* WHERE...Creek, Okfuskee, Okmulgee, McIntosh, Pittsburg,\nLatimer, Pushmataha, and Choctaw Counties.\n* WHEN...From 1 PM to 8 PM CDT Thursday.\n* IMPACTS...The combination of hot temperatures and high\nhumidity will combine to create a dangerous situation in which\nheat illnesses are possible.", "tags": [ "Extreme temperature value" ] }, ... ] 公式のレスポンス例を参考にしていますが、このままでは取得する情報が多く、使わないものもあるので条件を絞ってリクエストURLを作成したいと思います。条件は以下の通り。 hourly以外は取得しない 温度は摂氏で取得 言語は日本対応で取得 hourly以外は取得しない パラメータのexcludeにcurrent minutely daily alertsを指定しhourly以外は取得しないように設定します。 カンマ区切りのスペースなしである必要があります。 温度は摂氏で取得 温度を摂氏で取得するにはパラメータunitsにmetricを指定します。 言語は日本対応で取得 パラメータlangにjaを指定します。 リクエストURLの確認 https://api.openweathermap.org/data/2.5/onecall?lat=35.65146&lon=139.63678&units=metric&exclude=current,minutely,daily,alerts&lang=ja&appid={キー} 作成したリクエストURLです。緯度経度は東京都のとある地点を使っています。(将来的に現在地から緯度経度を取得します) レスポンスをもとにModelを作成 { "lat": 35.6515, "lon": 139.6368, "timezone": "Asia/Tokyo", "timezone_offset": 32400, "hourly": [ { "dt": 1643446800, "temp": 7.16, "feels_like": 4.87, "pressure": 1009, "humidity": 50, "dew_point": -2.26, "uvi": 0, "clouds": 75, "visibility": 8073, "wind_speed": 3.37, "wind_deg": 102, "wind_gust": 5.11, "weather": [ { "id": 500, "main": "Rain", "description": "小雨", "icon": "10n" } ], "pop": 0.21, "rain": { "1h": 0.12 } }, .... } 作成したリクエストを送るとこのようにレスポンスが返ってきます。 今回はhourlyから現在時刻〜48時間分のdt、temp、pressureと、weatherのmainのみを取得します。 WeatherResponseModel.swift import Foundation struct WeatherResponseModel: Decodable { var hourly: [Hourly] } struct Hourly: Decodable { var dt: Double var temp: Double var pressure: Double var weather: [Weather] } struct Weather: Decodable { var main: String } リクエストModelを作成 WeatherRequestModel.swift import Foundation struct WeatherRequestModel { var lat: Double var lng: Double } 緯度経度を格納するリクエストモデルです。 実装 アーキテクチャ MVVM + リポジトリパターンを採用して今回は開発しています。 DataStore WeatherDataStore.swift import Foundation final class WeatherDataStore { private let baseUrl = "https://api.openweathermap.org/data/2.5/onecall" private let shared = URLSession.shared private let decoder = JSONDecoder() func fetchWeathers(requestModel: WeatherRequestModel) async throws -> WeatherResponseModel { let params = "lat=\(requestModel.lat)&lon=\(requestModel.lng)&units=metric&exclude=current,minutely,daily,alerts&lang=ja&appid=\(API_KEY)" let urlString = baseUrl + "?" + params guard let url = URL(string: urlString) else { throw NSError(domain: "Error fetch weathers.", code: -1) } let request = URLRequest(url: url) let (data, _) = try await shared.data(for: request) let response = try decoder.decode(WeatherResponseModel.self, from: data) return response } } APIと通信するDataStoreです。今回async/awaitを使って非同期処理を書いています。 WeatherWidgetTests.swift import XCTest class WeatherWidgetTests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testWeatherDataStore() async throws { let dataStore = WeatherDataStore() let request = WeatherRequestModel(lat: 35.65146, lng: 139.63678) let response = try await dataStore.fetchWeathers(requestModel: request) print("Success fetch weathers.") print("Response hourly:", response.hourly) XCTAssert(response.hourly.count > 0) } } データに依存するので完全なテストではないですが、動作確認レベルでDataStoreのテストを書きました。このテストでAPI通信の挙動を確認できるので、テストが成功して正常に値が取得できているのであればData層の開発は完了です。 Repository WeatherRepositoryInterface.swift import Foundation protocol WeatherRepositoryInterface { func fetchWeathers(requestModel: WeatherRequestModel) async throws -> WeatherResponseModel } WeatherRepository.swift import Foundation class WeatherRepository: WeatherRepositoryInterface { private let weatherDataStore = WeatherDataStore() func fetchWeathers(requestModel: WeatherRequestModel) async throws -> WeatherResponseModel { do { let response = try await weatherDataStore.fetchWeathers(requestModel: requestModel) return response } catch { throw error } } } WeatherDataStoreへrequestModelを渡してdo catchで値を受け取ります。受け取った結果はasync throwsでViewModelに返します。後々開発するWidget ExtensionからもこのRepositoryを経由して天気情報を取得します。 ViewModel RepositoryRocator.swift import Foundation class RepositoryRocator { static func getWeatherRepository() -> WeatherRepositoryInterface { WeatherRepository() } } WeatherViewModel.swift import Foundation class WeatherViewModel: NSObject { private let weatherRepository: WeatherRepositoryInterface init(weatherRepository: WeatherRepositoryInterface) { self.weatherRepository = weatherRepository super.init() } override convenience init() { self.init(weatherRepository: RepositoryRocator.getWeatherRepository()) } func createRequestModel() -> WeatherRequestModel { let requestModel = WeatherRequestModel( lat: 35.65146, lng: 139.63678 ) return requestModel } func fetchWeathers() async { do { let response = try await weatherRepository.fetchWeathers(requestModel: createRequestModel()) print("Success fetch weathers:", response.hourly) } catch { print("Error fetch weathers:", error) } } } WeatherRepositoryへアクセスするViewModelです。RepositoryはRepositoryRocatorを経由してRepositoryInterface(抽象)から取得しています。 View ContentView.swift import SwiftUI struct ContentView: View { private let weatherVM = WeatherViewModel() var body: some View { Button { Task { await weatherVM.fetchWeathers() } } label: { Text("天気情報取得") .font(.system(size: 18, weight: .medium, design: .default)) .padding(.horizontal, 80) .padding(.vertical, 12) .foregroundColor(.white) .background(.orange) .cornerRadius(100) } } } 今回はウィジェットの開発がメインなのでアプリのViewはボタンタップで天気情報の取得だけにしておきます。 レスポンスのログ確認 Success fetch weathers: [ WeatherWidget.Hourly(dt: 1643612400.0, temp: 8.05, pressure: 1013.0, weather: [WeatherWidget.Weather(main: "Clouds")]), WeatherWidget.Hourly(dt: 1643616000.0, temp: 8.21, pressure: 1013.0, weather: [WeatherWidget.Weather(main: "Clouds")]), WeatherWidget.Hourly(dt: 1643619600.0, temp: 7.85, pressure: 1014.0, weather: [WeatherWidget.Weather(main: "Clouds")]), ... ] 48時間分の時間、気温、気圧、weatherが取得できているので取得成功です。 おわりに 今回はWidgetKitで天気予報アプリ作ってみた①〜天気情報取得編〜について書きました。 現状、緯度経度を直接指定しているので、WidgetKitで天気予報アプリ作ってみた〜位置情報取得&保存編〜で書き換えていきます。 続きが気になる方は↓こちら↓から ご覧いただきありがとうございました。 こうしたほうがいいや、ここはちょっと違うなど気になる箇所があった場合、ご教示いただけると幸いです。 お知らせ 現在副業でiOSアプリ開発案件を募集しています。 Twitter DMでご依頼お待ちしております! ↓活動リンクはこちら↓ https://linktr.ee/sasaki.ken
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode 13.3 - アプリ内課金でApp Store にコンテンツをホストする機能が廃止された

おさらい アプリ内課金コンテンツのホスト機能とは、非消費型プロダクトがコンテンツを持つ場合、それをアプリ内にバンドルするのではなく、App Storeサーバに置かせてもらう機能です。 アプリ本体のサイズを小さくし、購入したコンテンツだけを必要に応じてダウンロードさせる仕組みです。StoreKitフレームワークに属します。 アプリのバイナリとは別に、アプリ内課金コンテンツもバイナリとして生成する必要があり、生成したバイナリは Xcode Organizer経由でアップロードする必要がありました。 廃止になった Xcode 13.3 beta リリースノートを見ていたらさらっとサポート終了と書かれてました。 Building and uploading nonconsumable in-app purchase content for Apple to host is no longer supported. ちなみに既にホスト済みのコンテンツには影響しないとのこと。 実際に旧バージョン / 新バージョンの Xcode で比較してみました。 旧バージョン Xcode 13.2.1 新バージョン Xcode 13.3 beta 見ての通り、もうプロジェクトの作成自体ができなくなっています。 また、既に存在するプロジェクトでビルドしてみましたが失敗しました。 すなわちバイナリを生成することができません。 On-Demand Resources 代替機能としてOn-Demand Resourcesが利用できます。こちらの機能は iOS 9 から利用可能です。 ドキュメントを読んでみた感じでは、アプリ内課金コンテンツでなくともホストできる感じでした。利用方法も簡単そうです。ホストしたいコンテンツはアプリ本体と一緒にアーカイブします。バイナリをアップロードすると App Store側でスライスする際にコンテンツを分離する仕組みとなっています。 ダウンロード時にはFoundationフレームワークのAPIを利用します。 既存アプリでの対処 ホスト機能を実装済みで、今後もコンテンツを追加したいアプリの場合はどうすれば良いか考えてみました。 既にホスト済みのコンテンツは影響を受けませんが、追加コンテンツとの差分が発生するので悩みどころです。 案1) 新コンテンツのみ On-Demand Resourcesにする 案2) これを機に全コンテンツを On-Demand Resourcesにする 案3) 自社サーバ、または Firebase などの mBaasに乗り換える 私は案2が好きです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift×AWS】iOSアプリにAPI Gatewayを導入してCognitoから指定のユーザーデータを消去する

前提 ユーザーの退会機能を作るべく、今までAmplifyを使用していたアプリにAPI Gatewayを追加したサーバレスアプリです。 Amplifyからユーザーの削除APIがあればよかったのですが見つからずで、Lambdaから実現することに。(もしAmplifyからのやり方を知っている人がいたら教えてください笑) 元々はLambdaが使えれば良く、どうやらLambdaを直叩きする方法もあるっぽいんですが、API Gatewayを触ったことがなかったのでそちらを使って実装してみることにしました。 また、今回初めてAPI Gatewayを触るので、とりあえずリクエストだけ実装してレスポンスはデフォルトです。 実装が甘いところ等もあると思いますがご容赦ください。 構成 構成はこんな感じ。これをAlamofireを使って叩いていきます。 実際の設計にはユーザーの他のデータも消すため、Lambdaの先にDynamo DBなりS3なりがくっついているのですが、今回はCognitoのユーザーを消すことが目的なので割愛。 気が向いたらそちらも記事にします。 以下、各所説明。 ◆API Gateway 今回の主役です。APIがユーザー側から叩かれたらIDトークンを検証して、トークンの中にあるsub値をLambdaに渡します。そしてそれに応じてレスポンスをユーザー側に返していきます。 ◆AWS Lambda API Gatewayから呼ばれたら、送られてきたsub値を元に該当するcognitoユーザーを消去します。 実装方法 ◆Lambda API Gateway実装時に、Lambda関数を指定するため、先に該当するLambda関数を作成しておく。 ①Lambda関数を作成する。今回はNode.jsを使用して実装しますが、お好きなのを選択してください。 関数名は今回は"delete_user"とします。 ②関数を作成したら、Lambda関数がcognitoにアクセスできるようにロールにポリシーを振る。 設定タブにあるロールをクリック。 ③「ポリシーをアタッチします」をクリックし、「AmazonCognitoPowerUser」をアタッチ。 ④コードを実装する。私は以下のコードで実装しました。 let aws = require("aws-sdk"); exports.handler = async (event) => { // eventから削除するCognitoユーザーのユーザー名を格納 let userName = event.context.sub; // Cognitoユーザーの無効化メソッドをコール await disalbeCognito(userName); const response = { statusCode: 200, }; return response; }; // Cognito情報を無効化し、ログインできないようにする async function disalbeCognito(username){ // Cognitoを使う準備 aws.config.update({ region: 'ap-northeast-1', }); const cognito = new aws.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' }); // 対象のCognitoユーザープールID const user_pool_id = "ap-northeast-1_xxxxxxxx"; // ユーザー削除 try { await cognito.adminDeleteUser({ UserPoolId: user_pool_id, Username: username }).promise(); console.log("Success! userName : " + username); } catch (err) { console.log("Failed! userName : " + username); if (err.code == 'UserNotFoundException') { // ユーザープールにユーザーが存在していない場合 console.log('UserNotFoundException'); } else { // その他のエラー console.log(err, err.stack); } throw err; } } 使用するならコピペで大丈夫ですが、一箇所注意点があるのでお伝えしておきます。 let userName = event.context.sub; 4行目のこちらですが、これはAPI Gatewayの中にあるJSONの影響によってこうなっています。JSONの形式はAPI Gatewayの方で載せてある、そちらを確認してみてください。 こちらでデプロイしてLambdaは完了です。 ◆API Gateway ①AWSコンソール画面からAPI Gatewayを開き、REST APIの構築を選択。 ②プロトコル-REST、新しいAPIの作成を選択、API名(わかりやすい名前)を入力し、APIの作成をクリック。 ③アクションをクリックしメソッドの作成を選択。 ④作成されたメソッドを選択し、該当するものを選択。今回はユーザーを削除したいため「DELETE」を選択。その後、チェックマークをクリック。 ⑤作成されたメソッドのセットアップ。ここで先ほど作成したLambdaを指定し、保存をクリック。 ⑥"オーソライザー"タブを選択し、「新しいオーソライザーの作成」を選択し、作成。 ※ここで、消去したいユーザーが存在するユーザープールを選択。 ※また、トークンのソースは"Authorization"を入力したが、値はお好きなもので。 ⑦リソース→作成したメソッド→メソッドリクエストをクリックし、"認可"の右側の鉛筆マークから⑥で作成したおーそライザーを選択し、チェック。 ⑧統合リクエスト内下部のマッピングテンプレートから、「マッピングテンプレートの追加」を選択、Content-typeに"application/json"と入力しチェック。 ⑨テンプレートに以下のコードを入力し保存。 { "context" : { "sub" : "$context.authorizer.claims.sub" } } 上記により、IDトークン内のsubの値を取り出してLambdaに渡すことができます。 ここのJSONの形がcontext : { sub : {} } なので、Lambdaのeventの後にcontext.subと記述しています。 ⑩アクション→APIのデプロイを選択し、新しくステージを作成してデプロイすれば、完了です。 ※この手順で作るURLの形は決まっていて、以下のようになっています。 https://{restapi_id}.execute-api.{region}.amazonaws.com/{stage_name}/ そのため、stage名もある程度わかりやすいor命名規則を設けた方が良いでしょう。 ◆Swift Swift側の実装はメインではないので、さっくりコードだけ載せようと思います。 public func deleteAPI( completion: (()->())?=nil) { let urlString = "https://{restapi_id}.execute-api.{region}.amazonaws.com/{stage_name}/" let defaultHeader = ["Authorization" : "Bearer \(cognito.idToken)"] as HTTPHeaders Alamofire.request(urlString, method: .delete, headers: defaultHeader) .responseJSON() { (response) in switch response.result { case .success: print("success") break case .failure: print("failure") break } } } 以上で、実装終了です。 感想 駆け足でしたが、いかがだったでしょうか。 分かりくいところもあったと思いますが、筆者もそこまで深く理解できているわけではないのでご容赦ください。 何かありましたら、コメントで教えてくださると助かります。 API Gatewayは初めて触ってみたのですが、使いやすくて良いですね。何より便利です。 サーバレスってことでAPI Gatewayが使えてくると元々使えているAmplifyも使ってバックエンド側もガンガン構築できそうです。 それでは。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像をコラージュする

画像を組み合わせて一枚の画像を作る方法です 複数の画像から新しい画像を作りたい 複数の画像を組み合わせて一枚の画像にするとなると、はて、どうすればいいのか、、、と改めて思いました。 Contextに描画すればいい UIGraphicsBeginImageContextWithOptions(CGSize(width: outputWidth, height: outputHeight), false, 0.0) imageA.draw(in: drawRectA) imageB.draw(in: drawRectB) imageC.draw(in: drawRectC) let assembledImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホアプリ クロスプラットフォーム開発 in Flutter

Index 1.Flutterフレームワーク 2.AndroidStudioで開発できる 3.UIの記載はネストを多用する 4.UIとビジネスロジックを分けて書く 5.KotlinとSwiftによる書き分けが必要な場合がある 6.Dartプログラムのエントリーポイント 7.使用パッケージの宣言 8.ホットリロードで一回一回ビルドしなくてもいい 9.動作確認 10.アプリ公開 1.Flutterフレームワーク 2018年12月にGoogleからリリースされた、モバイルアプリ開発用のフレームワーク。 Dart言語 従来ならば、AndroidならKotlin iOSであればSwift という、それぞれの言語を覚えなければいけないが Flutterの場合は、DartというJavaライクな言語を覚えるだけで 両方の開発ができ、学習コストを抑えることができる。 2.AndroidStudioで開発できる AndroidStudioのインストール Flutterのインストール 3.UIの記載はネストを多用する 画面を構成するコンポーネントはWidgetというクラスが基底クラスにあり 各コンポーネントはWidgetを継承する。 Widgetを並べることで画面を作っていく。 コード const Text.rich( TextSpan( text: 'Hello', // default text style children: <TextSpan>[ TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), ], ), ) 4.UIとビジネスロジックを分けて書く BLoC(BusinessLogicComponent)というアーキテクチャを用いることで、UIとビジネスロジックを分けて書くことができるようになる BLoCパターンを用いることにより、次のことができるようになる ・ソースファイルごとの責務を明確化し、ソースの見通しがよくなる ・イベントハンドラ(StreamBuilder)を使って、状態管理をできる 参考URL)長めだけどたぶんわかりやすいBLoCパターンの解説 https://qiita.com/kabochapo/items/8738223894fb74f952d3 5.KotlinとSwiftによる書き分けが必要な場合がある UI部分、HTTP通信、内部ストレージへの保存などは一つのソースで書くことができるが 担当したプロダクトにおいてFirebaseとAzureNotificationHubsを利用したプッシュ通知をすることがあり、 Firebaseに端末を登録をするところまではDart記載で共通だが、 プッシュ通知の送信先を指示する部分であるAzureNotificationHubsへの端末識別情報の登録部分は、 Dartが対応していない部分であり、KotlinとSwiftで書き分ける必要がある。 6.Dartプログラムのエントリーポイント 以下のように、C言語などと同じで、main()から始まる // Copyright 2018 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Welcome to Flutter', home: Scaffold( appBar: AppBar( title: const Text('Welcome to Flutter'), ), body: const Center( child: Text('Hello World'), ), ), ); } } 参考URL) https://docs.flutter.dev/get-started/codelab 7.依存パッケージの宣言 pubspec.yamlという設定ファイルで、HTTP通信や内部ストレージアクセスなど、使用するパッケージを宣言する。 また、「version」でアプリバージョンを設定する name: <project name> description: A new Flutter project. publish_to: 'none' version: 1.0.0+1 environment: sdk: ">=2.7.0 <3.0.0" dependencies: flutter: # Required for every Flutter project sdk: flutter # Required for every Flutter project http: '>=0.12.0 <0.13.0' cupertino_icons: ^1.0.2 # Only required if you use Cupertino (iOS style) icons dev_dependencies: flutter_test: sdk: flutter # Required for a Flutter project that includes tests flutter: uses-material-design: true # Required if you use the Material icon font assets: # Lists assets, such as image files - images/a_dot_burr.jpeg - images/a_dot_ham.jpeg fonts: # Required if your app uses custom fonts - family: Schyler fonts: - asset: fonts/Schyler-Regular.ttf - asset: fonts/Schyler-Italic.ttf style: italic - family: Trajan Pro fonts: - asset: fonts/TrajanPro.ttf - asset: fonts/TrajanPro_Bold.ttf weight: 700 参考URL) https://docs.flutter.dev/development/tools/pubspec 宣言したパッケージはビルド時にダウンロードされる Flutterルートに.pubspecという隠しフォルダができ そこにパッケージがダウンロードされてビルドに使用される 8.ホットリロードで一回一回ビルドしなくてもいい Flutterにはホットリロードという機能があり UIを修正した際に修正が即反映される main関数を再度実行することなく、イベントハンドラの部分のみが処理される ①メインカラーをblueに指定 ②タイトルやボタンの色がblue ③メインカラーをyellowに指定してソースを保存 ④タイトルやボタンの色がyellow 9.動作確認 Androidのエミュレータ、実機 iPhoneのシミュレータ、実機 で確認を行うことができます。 Androidエミュレータ起動 iPhoneシミュレータ起動 10.アプリ公開 テスト完了後、アプリ審査に提出 AndroidであればGooglePlayConsole、 iOSであればAppleDeveloperから 作成したアプリを審査に提出する アプリの作成ルールに違反していないか審査され、問題がなければ公開の流れとなる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む