- 投稿日:2021-08-09T23:58:30+09:00
Swift初学者が自動販売機ロジックを書いてみたら2…(リファクタリング後)
今回は自動販売機ロジックについて学習したのでアウトプットします。 今回はその2回目です。 ちなみに私はMac触り始めて半年の者です。(ガチガチの初心者です) 初学者のアウトプット記事ですので、間違っている点など多々あるかと思いますが、暖かい目で見守っていただけますと幸いです。 はじめに 現在、私はMENTAで、ヤマタクメンターにご指導いただいております。 ヤマタクメンターのURL https://menta.work/user/1840 今回は課題として、自動販売機のロジックをXcodeのPlaygroundでコーディングしてみました。 簡単に言うと自動販売機の仕組みをコードで表現しましたみたいなことですね。 第一回目のコード Swift初学者が自動販売機ロジックを書いてみたら…(その1) 一発目から一回リファクタリングしてみたんですが、どうも納得いかなくてサンプルコードを参考にして、結局2回くらい書き直しました。 仕様 ・お金を入力する ・お金は1、5、10、50、100、500、1000円から何枚入力するか指定する ・1、5円は使用不可、その他のお金のみ使用可能とする ・水、コーヒー、エナジードリンクの3種類から指定する ・飲み物が買われたら、その飲み物の在庫数を-1して、お釣りを出力 ・お金が足りなければ、お金が足りません!と出力 ・在庫がなければ、すみません!在庫がありません!と出力 実装コード VendingMachine.playground import UIKit //ドリンクの種類と金額 enum DrinkType { case coffee case water case energyDrink var price: Int { switch self { case .coffee: return 150 case .water: return 100 case .energyDrink: return 200 } } } struct DrinkModel { var type: DrinkType var stock: Int } struct calculateMoney { var inputedYen: Int = 0 var change:Int = 0 enum Money: Int { case oneYen = 1 case fiveYen = 5 case tenYen = 10 case fiftyYen = 50 case onehundredYen = 100 case fivehundredYen = 500 case onethousandYen = 1000 } func MoneyCount(moneyType: Money,count: Int) -> Int { switch moneyType { case .oneYen: return Money.oneYen.rawValue * count case .fiveYen: return Money.fiveYen.rawValue * count case .tenYen: return Money.tenYen.rawValue * count case .fiftyYen: return Money.fiftyYen.rawValue * count case .onehundredYen: return Money.onehundredYen.rawValue * count case .fivehundredYen: return Money.fivehundredYen.rawValue * count case .onethousandYen: return Money.onethousandYen.rawValue * count } } //使用不可のお金が入力されているか判別 func selectDisabledMoney() -> Bool { guard oneYenCalculate > 0 || fiveYenCalculate > 0 else { return false } return true } //使用不可のお金は出力して、使用可能なお金はinputedYenとして代入する mutating func checkInputedYen() { if selectDisabledMoney() { inputedYen = tenYenCalculate + fiftyYenCalculate + onehundredYenCalculate + fivehundredYenCalculate + onethousandYenCalculate print("1円と5円は使用できません。\(oneYenCalculate + fiveYenCalculate)円お返しします。") }else{ inputedYen = tenYenCalculate + fiftyYenCalculate + onehundredYenCalculate + fivehundredYenCalculate + onethousandYenCalculate } } mutating func calculateChange(money: Int,price: Int) { change = money - price } } var calculate = calculateMoney() //何円を何枚使用するか入力 let oneYenCalculate = calculate.MoneyCount(moneyType: .oneYen, count: 0) let fiveYenCalculate = calculate.MoneyCount(moneyType: .fiveYen, count: 0) let tenYenCalculate = calculate.MoneyCount(moneyType: .tenYen, count: 0) let fiftyYenCalculate = calculate.MoneyCount(moneyType: .fiftyYen, count: 0) let onehundredYenCalculate = calculate.MoneyCount(moneyType: .onehundredYen, count: 0) let fivehundredYenCalculate = calculate.MoneyCount(moneyType: .fivehundredYen, count: 0) let onethousandYenCalculate = calculate.MoneyCount(moneyType: .onethousandYen, count: 1) //ドリンクが購入できることを確証する protocol BuyDrinkValidatable {} extension BuyDrinkValidatable { func validateDrinkBuyable(with drink: DrinkModel,inputYen: Int,change: Int) -> Bool { if drink.stock > .zero && drink.type.price <= inputYen && change >= 0 { return true }else{ return false } } } class VendingMachine: BuyDrinkValidatable { // ドリンクの初期在庫数 var coffee = DrinkModel(type: .coffee, stock: 5) var water = DrinkModel(type: .water, stock: 5) var energyDrink = DrinkModel(type: .energyDrink, stock: 5) // ドリンクを購入する関数 func buyDrink(type: DrinkType, inputedYen: Int) -> Bool { calculate.checkInputedYen() switch type { case .coffee: let isBuyable = validateDrinkBuyable(with: coffee, inputYen: calculate.inputedYen, change: calculate.change) if isBuyable { reduceStock(type: type) calculate.calculateChange(money: calculate.inputedYen, price: DrinkType.coffee.price) print("\(type)をどうぞ!\(calculate.change)円のお返しです!") }else if coffee.stock == .zero{ print("すみません在庫切れです!\(calculate.inputedYen)円お返しします") }else{ print("お金が足りません!\(calculate.inputedYen)円お返しします") } return isBuyable case .water: let isBuyable = validateDrinkBuyable(with: water, inputYen: inputedYen, change: calculate.change) if isBuyable { reduceStock(type: type) calculate.calculateChange(money: calculate.inputedYen, price: DrinkType.water.price) print("\(type)をどうぞ!\(calculate.change)円のお返しです!") }else if water.stock == .zero{ print("すみません在庫切れです!\(calculate.inputedYen)円お返しします") }else{ print("お金が足りません!\(calculate.inputedYen)円お返しします") } return isBuyable case .energyDrink: let isBuyable = validateDrinkBuyable(with: energyDrink, inputYen: inputedYen, change: calculate.change) if isBuyable { reduceStock(type: type) calculate.calculateChange(money: calculate.inputedYen, price: DrinkType.energyDrink.price) print("\(type)をどうぞ!\(calculate.change)円のお返しです!") }else if energyDrink.stock == .zero{ print("すみません在庫切れです!\(calculate.inputedYen)円お返しします") }else{ print("お金が足りません!\(calculate.inputedYen)円お返しします") } return isBuyable } } // 在庫を減らす関数 func reduceStock(type: DrinkType) { switch type { case .coffee: coffee.stock -= 1 case .water: water.stock -= 1 case .energyDrink: energyDrink.stock -= 1 } } } let activateVendingMachine = VendingMachine() //type引数にドリンク名を入れる activateVendingMachine.buyDrink(type: .coffee, inputedYen: calculate.inputedYen) おわりに 反省点として、 ・実装予定だった釣り銭切れの機能が実装できなかった ・ドリンクとお金の入力箇所が離れている ・キレイにまとめるつもりが、結果的に可読性の低いコードになってしまった 釣り銭切れの機能は、釣り銭が100円だとしたら、 100円1枚、50円2枚、10円10枚、10円5枚と50円1枚と、100円だけでも4パターン考えられ、1000円以上入力されるとなると、全てのパターンを網羅する必要があり、良い方法を見つけることができなかったので諦めました…泣 また、ユーザーの入力箇所が離れているのと、コードが冗長で、可読性が低いなと感じました。 他にも、汎用性や拡張性はどうなの?とか考えているとキリがないですし、今回のビジネスロジック実装でとても勉強になったので、気を取り直して次の課題に進みます!
- 投稿日:2021-08-09T23:34:06+09:00
NavigationBarにItem(ボタン)を追加 押したらiPhoneのホーム画面のドックみたいなUIViewを下から表示
完成形 機能説明 NavigationBarの右端に設置したItemを押すと、下からiPhoneのドックみたいなUIViewを表示させます。 コードと簡単解説 NavigationBarにItem(ボタン)を追加 NavigationBarの右端に.actionアイコンを設定して追加しています。 action: #selector(showUnderView)は、UIViewを上げ下げする為の処理を設定しています。 override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(showUnderView)) } 0.3秒でUIViewとUIButtonをanimations:{}で指定した位置まで移動させます。 ~~~一部省略~~~ var underView = UIView() var underViewButton = UIButton() ~~~一部省略~~~ @objc func showUnderView(){ if underView.frame.origin.y == self.view.frame.maxY{ UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underView.frame.origin.y = self.view.frame.maxY - (self.view.frame.maxY / 7.5)}, completion: nil) UIButton.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underViewButton.frame.origin.y = self.underView.frame.origin.y + 10}, completion: nil) }else if underView.frame.origin.y == self.view.frame.maxY - (self.view.frame.maxY / 7.5){ UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underView.frame.origin.y = self.view.frame.maxY}, completion: nil) UIButton.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underViewButton.frame.origin.y = self.underView.frame.origin.y + 3}, completion: nil) } } iPhoneのホーム画面のドックみたいなUIViewを作成 ホーム画面のドックみたいなUIViewを作成 ~~~一部省略~~~ var underView = UIView() ~~~一部省略~~~ func createUnderView(){ underView = UIView(frame: CGRect(x: view.frame.minX + 5, y: self.view.frame.maxY, width: view.frame.size.width - 10, height: view.frame.size.height / 9)) underView.layer.cornerRadius = 15.0 //背景色を設定 underView.backgroundColor = .systemGray2 underView.alpha = 0.35 //透過度を設定 view.addSubview(underView) } 全てのコード ViewController import UIKit class ViewController: UIViewController { var underView = UIView() var underViewButton = UIButton() override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(showUnderView)) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) createUnderView() createUnderViewButton(yPoint: underView.frame.origin.y) } @objc func showUnderView(){ if underView.frame.origin.y == self.view.frame.maxY{ UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underView.frame.origin.y = self.view.frame.maxY - (self.view.frame.maxY / 7.5)}, completion: nil) UIButton.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underViewButton.frame.origin.y = self.underView.frame.origin.y + 10}, completion: nil) }else if underView.frame.origin.y == self.view.frame.maxY - (self.view.frame.maxY / 7.5){ UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underView.frame.origin.y = self.view.frame.maxY}, completion: nil) UIButton.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {self.underViewButton.frame.origin.y = self.underView.frame.origin.y + 3}, completion: nil) } } func createUnderView(){ underView = UIView(frame: CGRect(x: view.frame.minX + 5, y: self.view.frame.maxY, width: view.frame.size.width - 10, height: view.frame.size.height / 9)) underView.layer.cornerRadius = 15.0 underView.backgroundColor = .systemGray2 underView.alpha = 0.35 view.addSubview(underView) } func createUnderViewButton(yPoint:CGFloat){ underViewButton = UIButton(frame: CGRect(x: underView.frame.minX + 10, y: yPoint + 10, width: underView.frame.size.height - 20, height:underView.frame.size.height - 20)) underViewButton.backgroundColor = UIColor.systemIndigo underViewButton.layer.cornerRadius = 15.0 underViewButton.setTitle("Button", for: .normal) underViewButton.setTitleColor(.white, for: .normal) underViewButton.addTarget(self, action: #selector(test), for: .touchDown) view.addSubview(underViewButton) print("underViewButton表示") } @objc func test(){ print("underViewButton押されました") } } 終わり ご指摘、ご質問などありましたら、コメントまでお願い致します。
- 投稿日:2021-08-09T21:05:17+09:00
Swift パターンマッチ
通常Switch文で使用されると思われるパターンマッチだがifやguard文whileにも使用される if let age = 8 if case 1...12 = age { print("小学生です") } switch文では条件をすべて網羅しつつ、できないものはdefaultで拾う書き方をするがif文では網羅する必要がない際に使われる。 guard let age = 23 guard case 1...20 = age else { return } print("二十歳以下です") } while パターンに合うまではその処理を繰り返し回す。 var nextAge = Optional(1) while case let age? = nextAge { if age >= 20 { nextAge = nil } else { nextAge += 1 } }
- 投稿日:2021-08-09T18:02:24+09:00
Resultを使った例外処理
使い所 非同期処理ではdo-catchが使用できないので、そういった時に利用可能 enum DatabaseError : Error { case entryNotFound case duplicatedEntry case invalidEntry(reason: String) } struct User { let id: Int let name: String let email: String } func findUser(byID id: Int) -> Result<User, DatabaseError> { let users = [ User(id: 1, name: "yuika", email: "yuika@gmail.com"), User(id: 2, name: "misa", email: "misa@gmail.com"), User(id: 3, name: "niku", email: "niku@gmail.com") ] for user in users { if user.id == id { return .success(user) } } return .failure(.entryNotFound) } let result = findUser(byID: 4) switch result { case let .success(user): print(".success: \(user)") case let .failure(error): switch error { case .entryNotFound: print(".failure: .entryNotFound") case .duplicatedEntry: print(".failure: .duplicatedEntry") case .invalidEntry(let reason): print(".failure: .invalidEntry(\(reason))") } }
- 投稿日:2021-08-09T15:10:00+09:00
Swiftでの非同期処理
非同期処理 同期処理では上から行われるだけですが、非同期処理では並行処理が可能です。 同期処理では画像ダウンロード開始 ダウンロード完了 プログラム終了となりますが、 非同期処理でかくと画像ダウンロード開始 プログラム終了 ダウンロード完了 となり、重い処理を待たずに次の処理が可能です。 import Foundation import PlaygroundSupport import Dispatch import UIKit // Playground上で非同期処理を行えるようにする PlaygroundPage.current.needsIndefiniteExecution = true func downloadImage(url: URL, completion: @escaping(Data)->Void) { let request = URLRequest(url: url) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if let data = data { completion(data) } } task.resume() } var image : UIImage? print("画像ダウンロード開始") let url = URL(string: "https://www.apple.com/jp/iphone-12/images/meta/iphone-12_overview__e6j9bvy6778m_og.png")! downloadImage(url: url) { (data: Data) in image = UIImage(data: data) print("ダウンロード完了") } print("プログラム終了")
- 投稿日:2021-08-09T14:50:00+09:00
いらすとやガチャアプリを作成(swift)
概要 今や日常まで浸食しているシュールな絵で人気のいらすとや。 これらを集めてガチャアプリにしました。 構成 ①ガチャ景品素材を作成 ②Array[UIImage]を作成 ③ガチャ演出アニメーションを作成 ④SwiftGifを使用してgifの表示を可能にする ⑤アニメーションメソッドを作成 ⑥ガチャ景品排出メソッドを作成 ⑦メソッドを組み合わせて「ガチャを引く」ボタンを作成 ①ガチャ景品素材を作成 いらすとやから独断と偏見で面白いと思った画像を70個選定しました。 これらをkeynoteでいい感じにソシャゲっぽっくします。 SSR・SR・R・Nを用意しました。 ②Arrayを作成 ガチャ景品素材を格納する配列を作成します。 ViewController.swift var gachaImageArray: [UIImage] = [ UIImage(named: "chunigirl")!, //70個分記入 ] ③ガチャ演出アニメーションを作成 keynoteでgifを作成しました。 ・カプセルがドロップするアニメーション ・カプセルが開くアニメーション ④SwiftGifを使用してgifの表示を可能にする swiftではそのままではgif画像を扱うことができません。 SwiftGifというライブラリをcocoapodsでインストールして使用させていただきました。 https://github.com/swiftgif/SwiftGif ViewController.swift import SwiftGifOrigin ⑥アニメーションメソッドを作成 ViewController.swift func gachaAnimation (){ //隠しておいたカプセルのUIImageViewを表示する self.capcelImageView.isHidden = false //swiftGifライブラリをインポートすることにより、.loadGif(name:引数)が使用可能に //1つ目のアニメーションを表示 capcelImageView.loadGif(name: "capcelgif") //2つ目のアニメーションを表示 //一つ目が終わった後に表示させるために遅延処理する DispatchQueue.main.asyncAfter(deadline: .now() + 3.75) { self.capcelImageView.loadGif(name: "capcelgif2") } } ⑤ガチャ景品排出メソッドを作成 ViewController.swift //ガチャの結果を表示する機能 func gachaResult(){ //結果の表示 アニメーションの後に表示するため遅延処理 DispatchQueue.main.asyncAfter(deadline: .now() + 5.25) { //配列からランダムに画像を取り出し、取り出した画像をUIImageViewにセット self.gachaImageView.image = self.gachaImageArray.randomElement() //結果を表示 カプセルのアニメーションを非表示 self.gachaImageView.isHidden = false self.capcelImageView.isHidden = true //ボタンを再度表示させる。 self.lottery1Button.isHidden = false //2回目以降はボタンの文字を変える self.lottery1Button.setTitle("もう一度引く", for: .normal) } 排出確率は一定です。1/70の確率で何かが出ます。 ⑦メソッドを組み合わせて「ガチャを引く」ボタンを作成 ViewController.swift //ガチャを引くボタン @IBAction func lottery1Button(_ sender: Any) { //初期化(タイトル以外のbuttonとimageを一旦消す) lottery1Button.isHidden = true gachaImageView.isHidden = true capcelImageView.isHidden = true gachaStartImageView.isHidden = true //ガチャ機能 gachaAnimation() gachaResult() //Timeline(dispatch que) //after 0 second : gacha drop(capcelgif animation) animationtime is 3.75 //after 3.75 seconds :gacha open(capcelgif2 animarion) animationtime is 2.0 //after 5.25 seconds : gacha open animation is hidden //after 5.25 seconds : result display //引いた回数をプラス1する counter += counter } ボタンを押すと、アニメーション後に下記の様に表示 今後の課題 ・確率操作(SSRの景品の排出確率を0.1%にするなど)ができない。 ・引いた景品を記録して、コレクション表示させたい ・引けるのは1日10回まで、それ以降は課金などの機能
- 投稿日:2021-08-09T14:37:09+09:00
【備忘録】Firebase Dynamic Links でサンプルアプリを起動させる (iOS)
概要 Firebase Dynamic Linksを使ってアプリ(サンプルアプリ)を起動させる 知っておくと助かるかもしれないこと 環境 Xcode 12.4 Swift 5 Firebase Dynamic Links 必要なことは全てFirebaseが教えてくれた Firebase を iOS プロジェクトに追加する https://firebase.google.com/docs/ios/setup?hl=ja Dynamic Links作成方法 https://firebase.google.com/docs/dynamic-links/ios/create?hl=ja Dynamic Links受信方法(iOS) https://firebase.google.com/docs/dynamic-links/ios/receive?hl=ja ちゃんと手順に従ってやっていけば普通は問題なくできる 知っておくと助かるかもしれないこと サンプルアプリの場合、 Firebaseのプロジェクト設定でApp Store IDの入力迫られるが、 Storeにあげてなくても大丈夫 ストアに上がっている適当なアプリのIDを入力しておけばOK Firebaseのアプリ設定のチームIDは、Provisioning ProfileのApp IDの最初の.までの部分 Xcodeで見れるチームIDと同じパターンと違うパターンがあるらしいので要確認 Provisioning Profileは Finder > ライブラリ > MobileDevice > Provisioning Profiles にある Dynamic Link設定時 アプリがインストールされていない場合のユーザーの移動先: App Store を選択している場合 リンクの設定をミスっている時はFirebaseのエラーページが表示されたが、 リンクが正しく設定されていればApp Storeが表示される よって、その時はXcode上で何かミスっている↓ Xcode上のAssociated Domains にドメインを追加するとき 「applinks:your_dynamic_links_domain」の「:」はひとつ なぜか2つにしててずっとアプリ遷移できなかった 作成したリンクに?d=1をつけて表示するとpreviewLinkが見れる 例) https://sample.page.link/AAA?d=1 こんな画面 リンクをそのままSafariとかで直打ちだと表示されない場合がある メールにリンク貼って送信しそのリンクからだと遷移できる
- 投稿日:2021-08-09T13:43:47+09:00
SceneDelegateで様々なアプリの起動経路に対応する
はじめに iOS13でSceneDelegateが導入されて久しいですが、 この度既存プロジェクトにSceneDelegateを導入する必要があったので、 AppDelegateに書かれていた処理を適宜SceneDelegateに書きScene対応しました。 今回は以下の起動経路に対応する必要があったので、それについて記載しています。 起動経路 アプリの状態 リモートプッシュ通知 アプリ未起動時に受信フォアグラウンド時に受信バックグラウンド時に受信 URLスキームによる起動 アプリ未起動時に受信バックグラウンド時に受信 Quick Action(3D Touch) アプリ未起動時に受信バックグラウンド時に受信 Spotlight検索 アプリ未起動時に受信バックグラウンド時に受信 SceneDelegateとは iOS13からUISceneというクラスが追加され、 マルチウィンドウに対応できるようになりました。 iOS12まではアプリのプロセス(起動や終了などを指す)は1つで、 それに対するUIインスタンスも1つでした。 これがiOS13からはアプリのプロセスは1つで、 それに対するUIインスタンスは複数になりました。 このUIインスタンスをSceneといいます。 これまではアプリのプロセス、UIの状態管理を全てAppDelegateで行っていましたが、 このSceneの概念が登場したことに伴い、 AppDelegateではアプリ全体のプロセスのみを管理して、 新たに追加されたSceneDelegateで画面の表示などのUIの状態やライフサイクル管理を担うようになりました。 詳細は[iOS13] UIScene APIを使用する [Xcode11]の記事が大変参考になりました。 SceneDelegateのライフサイクル 【xcode 11】新たに導入されたsceneDelegateの各メソッドが呼ばれるタイミングの記事が具体的でわかりやすかったです。 実装ベースでライフサイクルのイメージを掴む事ができると思います。 導入の経緯 Widgetを入れるためにその前段としてSceneDelegateを導入しました。 実装 前提 サポート対象: iOS10以降 iOS13の前後で通知先がAppDelegateとSceneDelegateで分かれるため、両方実装しています。 リポートプッシュ通知はFirebase Cloud Messagingを利用しています windowの作成やInfo.plistの設定などは割愛します リモートプッシュ通知(Firebase Cloud Messaging) 各通知先 起動方法 通知先(~iOS12) 通知先(iOS13~) アプリ未起動時に受信 ①application(_:didFinishLaunchingWithOptions:) -> Bool ④func scene(_:willConnectTo:options:) フォアグラウンド時に受信 ②UNUserNotificationCenterDelegateの準拠先に通知func userNotificationCenter(_:didReceive: withCompletionHandler) ⑤UNUserNotificationCenterDelegateの準拠先に通知func userNotificationCenter(_:didReceive: withCompletionHandler) バックグラウンド時に受信 ③同上 ⑥同上 実装 AppDelegate.swift extension AppDelegate: UIApplicationDelegate, UIResponder { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if #available(iOS 13, *) {} else { // iOS12以下はAppDelegateをdelegate先に設定 UNUserNotificationCenter.current().delegate = self } let userInfo = launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any] // ① userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用 return true } } // MARK: - UNUserNotificationCenterDelegate extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo // ②③ userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用 completionHandler() } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .badge, .sound]) } } SceneDelegate.swift @available(iOS 13.0, *) extension SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // iOS13以上はSceneDelegateをdelegate先に設定 UNUserNotificationCenter.current().delegate = self // プッシュ通知による起動 if let response = connectionOptions.notificationResponse { let userInfo = response.notification.request.content.userInfo // ① userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用 } } } // MARK: - UNUserNotificationCenterDelegate @available(iOS 13.0, *) extension SceneDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo // ⑤⑥ userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用 completionHandler() } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .badge, .sound]) } } 備考 様々な記事を調べていたところ、 UNUserNotificationCenterDelegateのdelegate先はSceneDelegateを使っていたとしても、 共通でAppDelegateに準拠させるで良さそうだったのですが、 実際にiOS13以上で動かしてみるとSceneDelegateで準拠していないとプッシュを受け取れませんでした。 URLスキームによる起動 各通知先 起動方法 通知先(~iOS12) 通知先(iOS13~) アプリ未起動時に発火 func application(_:open:options:) -> Bool func scene(_:willConnectTo:options:)connectionOptions.urlContexts.first?.url バックグラウンド時に発火 func application(_:open:options:) -> Bool scene(_:openURLContexts:) 実装 AppDelegate.swift extension AppDelegate: UIApplicationDelegate, UIResponder { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { // ①② urlを使用 return true } } SceneDelegate.swift @available(iOS 13.0, *) extension SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // URLSchemeによる起動 if let url = connectionOptions.urlContexts.first?.url { // ③ } } func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { guard let url = URLContexts.first?.url else { return } // ④ } } Quick Action(3D Touch) 各通知先 起動方法 通知先(~iOS12) 通知先(iOS13~) アプリ未起動時に受信 ①func application(_:performActionFor:completionHandler:) ③func scene(_:willConnectTo:options:)connectionOptions.shortcutItem バックグラウンド時に受信 ②同上 ④func windowScene(_:,shortcutItem:completionHandler:) 実装 AppDelegate.swift extension AppDelegate: UIApplicationDelegate, UIResponder { func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { // ①② } } SceneDelegate.swift @available(iOS 13.0, *) extension SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Quick Actionによる起動 if let shortcutItem = connectionOptions.shortcutItem { // ③ } } func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { // ④ } } Spotlight検索 各通知先 起動方法 通知先(~iOS12) 通知先(iOS13~) アプリ未起動時に発火 ①func application(_:continue:restorationHandler:) -> Bool ③func scene(_:willConnectTo:options:)connectionOptions.userActivities.first バックグラウンド時に発火 ②同上 ④func scene(_:continue:) 実装 AppDelegate.swift extension AppDelegate: UIApplicationDelegate, UIResponder { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { // ①② return true // 受け取ったuserActivityをハンドリングするか否かを返す } } SceneDelegate.swift @available(iOS 13.0, *) extension SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // spotlight検索による起動 if let userActivity = connectionOptions.userActivities.first { // ③ } } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { // ④ } } その他SceneDelegateを扱う上での注意点 SceneDelegateを使う場合、UIWindowはAppDelegateではなくUIWindowSceneを使う UIWindow配下のrootViewControllerにアクセスする場合はSceneDelegate経由で取得する必要がある (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = vcではとれない Windowを新たに作成する場合に、UIWindowSceneから作成する必要がある 参考 参考 [iOS13] UIScene APIを使用する [Xcode11] 【xcode 11】新たに導入されたsceneDelegateの各メソッドが呼ばれるタイミング [iOS 14]WidgetでDeep Link作成 アプリ画面のライフサイクル管理をAppDelegateからSceneDelegateへ移行した話 肥大化しがちなアプリの起動経路を整理する
- 投稿日:2021-08-09T09:18:41+09:00
TabBarControllerの各タブを別のStoryboardへ分割する
はじめに StoryboardでTabBarControllerを追加した際に、各タブのViewControllerを別Storyboardに分けたい。 環境 Xcode 12.4 Swift 5.3.2 作業手順 対象のタブのViewControllerを選択し、「Editor」「Refactor to Storyboard」をクリック 分割するStoryboard名を決め、配下に置きたいフォルダーをGroupで選択する これで別のStoryboardに分けられました TabBarControllerのあるStoryboardで必要な分のViewControllerを追加し、同じく作業を繰り返す 結果 無事TabBarControllerのタブ二つをそれぞれ別のStoryboardに分けられました。 感想 今後も同じことをやりそうだったので、次回のためのメモです。 参考記事 特になし。 色々いじってみたら上手くいきました。 もっと別の方法があるのかもしれませんが。
- 投稿日:2021-08-09T05:09:52+09:00
Notificationを使ったオブザーバパターンを理解
Notification PosterとObserverは直接繋がっていないが、addObserverを使って、通知を監視し通知があった際に#selecterで指定した関数を実行可能です。 import Foundation class Poster { static let notificationName = Notification.Name("SomeNotification") func post(message: String) { NotificationCenter.default.post(name: Poster.notificationName, object: message) } } class Observer { init() { NotificationCenter.default.addObserver(self, selector: #selector(handleNotification(_:)), name: Poster.notificationName, object: nil) } deinit { NotificationCenter.default.removeObserver(self) } @objc func handleNotification(_ notification: Notification) { if let object = notification.object { print(object) } print("通知を受け取りました") } } var observer = Observer() let poster = Poster() poster.post(message: "Hello")
- 投稿日:2021-08-09T02:05:39+09:00
Cognito認証を経てS3バケットにアップロード
はじめに Cognitoでユーザープールでの認証完了後、IDプールと連携してS3バケットにアップロードするために必要なAWS設定を調べてみました。Cognitoユーザープールの設定やSwiftでのCognitoによるサインインやS3バケットへのアップロードを行うコードについては大幅に割愛していますのでご了承ください。 AWS設定手順 S3:バケットの作成 デフォルトの設定でバケットを作成します。 Cognito:IDプールの設定 CognitoのIDプールの管理画面から新しい ID プールの作成を押してIDプールを作成します。IDプール名を入力後、認証プロバイダーに作成済みのユーザープールIDとアプリクライアントIDを指定しプールの作成を押します。 次に表示される画面ではロールを作成します。上の段(Your authenticated identities...)のポリシードキュメントを表示し、編集ボタンを押して編集します。今回はアップロードすることが目的なので、"s3:PutObject*"を追加します。編集が終わったら画面下方の許可ボタンを押して次に進みます。 { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject*", "mobileanalytics:PutEvents", "cognito-sync:*", "cognito-identity:*" ], "Resource": [ "*" ] } ] } サンプルコードが表示されひとまず設定終了です。identityPoolIdが後々必要になりますが、この画面は後からでも確認することができます。 Swiftによる実装 パッケージの追加 podで以下のパッケージを追加します。 # For Amazon Cognito. pod 'AWSCognitoIdentityProvider', '~> 2.12.0' pod 'AWSS3' AppDelegate リージョンやIDなどを指定します。 import AWSCognitoIdentityProvider import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { private let REGION: AWSRegionType = リージョン private let USER_POOL_ID: String = ユーザープールID private let ID_POOL_ID: String = IDプールのサンプルコードのidentityPoolId private let APP_CLIENT_ID: String = アプリクライアントID private let APP_CLIENT_SECRET: String? = nil private let KEY: String = "UserPool" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let serviceConfiguration = AWSServiceConfiguration( region: REGION, credentialsProvider: nil ) let userPoolConfigration = AWSCognitoIdentityUserPoolConfiguration( clientId: APP_CLIENT_ID, clientSecret: APP_CLIENT_SECRET, poolId: USER_POOL_ID ) AWSCognitoIdentityUserPool.register( with: serviceConfiguration, userPoolConfiguration: userPoolConfigration, forKey: KEY ) // Amazon Cognito 認証情報プロバイダーを初期化します let pool = AWSCognitoIdentityUserPool(forKey: KEY) // IDプールで表示されるサンプルはidentityProviderManagerは指定されていませんが、指定した方が良いようです let credentialsProvider = AWSCognitoCredentialsProvider( regionType: REGION, identityPoolId: ID_POOL_ID, identityProviderManager:pool ) let configuration = AWSServiceConfiguration( region: REGION, credentialsProvider:credentialsProvider ) AWSServiceManager.default().defaultServiceConfiguration = configuration return true } CognitoユーザーによるサインインやS3アップロードのコードは割愛させていただきます。 その他 認証しないでアップロードを試みるとtransferUtility.uploadDataの実行時に以下のようなエラーが発生するのでおそらくうまく行っているのではないかと思っています。 Authentication delegate not set ツッコミどころ満載と思いますが、不備等あればご指摘いただければ幸いです。 参考文献 Amazon Cognito デベロッパーガイド
- 投稿日:2021-08-09T01:12:41+09:00
クロージャーを引数として渡す
渡される側の関数 func someFunction(complecation: (String, Int) -> Void) { complecation("Hello", 2) complecation("World", 3) } 引数complecationにクロージャーを渡す someFunction(complecation: { (text: String, number :Int) in for _ in 0..<number { print(text) } }) 引数ではなくクロージャーをそのまま指定 someFunction { (text: String, number :Int) in for _ in 0..<number { print(text) } } 引数名を使用を省略 someFunction(complecation: { for _ in 0..<$1 { print($0) } }) クロージャーを先に宣言 let closer = { (text: String, number :Int) in for _ in 0..<number{ print(text) } } someFunction(complecation: closer) 有用なクロージャーの使い方 Listの関数sort()内で比較するところを自分で実装できるようになります。 struct List<T> { var list: [T] init(_ list: [T]) { self.list = list } mutating func sort(compare: (T, T) -> Bool) { for i in 0..<list.count { for j in i..<list.count { if (compare(list[i], list[j])) { let tmp = list[i] list[i] = list[j] list[j] = tmp } } } } } var intList = List([4, 7, 3, 1, 12]) intList.sort {$0 < $1} intList.sort {$0 > $1} class People { let age: Int let testScore: Int init(_ age: Int,_ testScore: Int) { self.age = age self.testScore = testScore } } var peopleList : [People] = [] for i in 0..<5 { let people = People(i * 5, i * 20) peopleList.append(people) } var list = List(peopleList) list.sort {$0.age < $1.age} list.list.forEach { print($0.age, terminator: ", ") } print("") list.sort {$0.testScore > $1.testScore} list.list.forEach { print($0.testScore, terminator: ", ") }