- 投稿日:2020-03-28T22:58:32+09:00
Swiftでiosアプリ開発-1【個人記録】
概要
「たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 11 Swift 5対応」
に沿って、Swiftでのiosアプリ開発を学習してみることにしました。その記録用です。じゃんけんアプリ
楽器アプリ
パーツの配置
楽器アプリには、シンバル、ギター、Play、Stopというパーツがある。
それぞれ、のパーツは押すことによってプログラムが動作することが要求されるため、パーツはそれぞれButtonである。1.使う画像をAssets.xcassetsに格納する
これで、プログラムやAttributesInspectorから画像を呼び出せるようになる。
2.パーツを配置する
AttributesInspectorから+ボタンで、配置したいパーツを選択し、それをドラッグ&ドロップで配置していく。
3.配置されたパーツのレイアウトを変更する
AttributesInspectorで全て行うことができる。
Buttonには、取り込んだImageを反映することもでき、それもここで行う。
4.配置されたパーツのAutoLayoutを設定する
AutoLayoutというのは、様々なデバイスで適切にパーツが表示されるための便利な設定みたいなもの。
中央から垂直方向、水平方向にどれだけ距離があるかを設定することで、様々なデバイスで適切な見た目になるようにすることができる。
水平方向マイナス:左側
水平方向プラス:右側
垂直方向マイナス:上側
垂直方向プラス:下側に設定した値だけ、中央からパーツがずれていく。
例えば、ギターは、中央からみて、右上に配置したいので、
Horizontally in Container : 80
Vertically in Container : -80
となる。5.音源を準備する
a.音源ファイルをMyMusic配下に格納する
b.AVFoundationを読み込む
import UIKit //追加 import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. }AVfoundationとは、音や画像を扱いやすくしてくれるフレームワークである。
c.音源ファイルのパスと、AVfoundationのインスタンスを生成する
import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } //定数cymbalPathに、音源ファイルのパスを格納。Bundleクラスはファイルや画像を管理しているクラス。 let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3") //変数cymbalPlayerにAVVudioAVVudioplayerクラスのインスタンスを格納し、そのクラスのメソッドを使えるようにしておく。 var cymbalPlayer = AVAudioPlayer()6.パーツとプログラムを関連づける
a.パーツをCtrlキーを押したまま、ドラッグ&ドロップ
b.音を再生するプログラムを作成
import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3") var cymbalPlayer = AVAudioPlayer() //追加 @IBAction func cymbal(_ sender: Any) { //AVAudioplayerに音源ファイルを指定 do{ cymbalPlayer = try AVAudioPlayer(contentsOf:cymbalPath, fileTypeHint:nil) //音の再生 cymbalPlayer.play() }catch{ print("シンバルで、エラーが発生しました。") } } let guitarPath = Bundle.main.bundleURL.appendingPathComponent("guitar.mp3") var guitarPlayer = AVAudioPlayer() @IBAction func guitar(_ sender: Any) { do{ guitarPlayer = try AVAudioPlayer(contentsOf:guitarPath, fileTypeHint:nil) guitarPlayer.play() }catch{ print("ギターで、エラーが発生しました。") } } }例外処理
Swiftでは、例外が発生しうるクラスを利用する時は、例外処理を明示して書かないとエラーになってしまう。do { try メソッド呼び出し }catch { エラー処理 }のように書く。
AudioPlayerとかBundleとかクラスがあって、それを継承して、そこのメソッドを使うためにインスタンスを作成して音を鳴らしているというのは理解できるけれど、具体的に各クラスがどのようになっているのかまではわかっていない。
とりあえず今は先に進んで、あとで戻ってきたときに、そこは調べたいと思う。7.完成形
import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } //シンバルの音源のPathと、AVAudioPlayerのインスタンスを作成 let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3") var cymbalPlayer = AVAudioPlayer() //シンバル(ボタン)と関連づけ @IBAction func cymbal(_ sender: Any) { //AVAudioplayerに音源ファイルを指定 do{ cymbalPlayer = try AVAudioPlayer(contentsOf:cymbalPath, fileTypeHint:nil) //音の再生 cymbalPlayer.play() }catch{ print("シンバルで、エラーが発生しました。") } } //ギターの音源のPathと、AVAudioPlayerのインスタンスを作成 let guitarPath = Bundle.main.bundleURL.appendingPathComponent("guitar.mp3") var guitarPlayer = AVAudioPlayer() //ギター(ボタン)と関連づけ @IBAction func guitar(_ sender: Any) { //AVAudioplayerに音源ファイルを指定 do{ guitarPlayer = try AVAudioPlayer(contentsOf:guitarPath, fileTypeHint:nil) //音の再生 guitarPlayer.play() }catch{ print("ギターで、エラーが発生しました。") } } //BGMの音源のPathと、AVAudioPlayerのインスタンスを作成 let backmusicPath = Bundle.main.bundleURL.appendingPathComponent("backmusic.mp3") var backmusicPlayer = AVAudioPlayer() //Play(ボタン)との関連づけ @IBAction func play(_ sender: Any) { //AVAudioplayerに音源ファイルを指定 do{ backmusicPlayer = try AVAudioPlayer(contentsOf: backmusicPath, fileTypeHint: nil) //ボタン押下後の再生回数を定義 backmusicPlayer.numberOfLoops = -1 //音の再生 backmusicPlayer.play() }catch{ print("エラーが発生しました。") } } //Stop(ボタン)と関連づけ @IBAction func stop(_ sender: Any) { //BGMの停止 backmusicPlayer.stop() } }8.リファクタリング
共通部分(音楽再生やパスをAVAudioPlayerに渡したりするところ)が多いので、そこをメソッドにしてしまう。
あと、定数や変数の定義は、最初にしてしまった方がみやすい。import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } //共通部分をメソッド化 fileprivate func soundPlayer(player:inout AVAudioPlayer,path:URL,count: Int){ do{ player = try AVAudioPlayer(contentsOf: path, fileTypeHint: nil) player.numberOfLoops = count player.play() }catch{ print("エラーが発生しました") } } //シンバルの音源のPathと、AVAudioPlayerのインスタンスを作成 let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3") var cymbalPlayer = AVAudioPlayer() //ギターの音源のPathと、AVAudioPlayerのインスタンスを作成 let guitarPath = Bundle.main.bundleURL.appendingPathComponent("guitar.mp3") var guitarPlayer = AVAudioPlayer() //BGMの音源のPathと、AVAudioPlayerのインスタンスを作成 let backmusicPath = Bundle.main.bundleURL.appendingPathComponent("backmusic.mp3") var backmusicPlayer = AVAudioPlayer() //シンバル(ボタン)と関連づけ @IBAction func cymbal(_ sender: Any) { soundPlayer(player: &cymbalPlayer, path: cymbalPath, count: 0) } //ギター(ボタン)と関連づけ @IBAction func guitar(_ sender: Any) { soundPlayer(player: &guitarPlayer, path: guitarPath, count: 0) } //Play(ボタン)との関連づけ @IBAction func play(_ sender: Any) { soundPlayer(player: &backmusicPlayer, path: backmusicPath, count: -1) } //Stop(ボタン)と関連づけ @IBAction func stop(_ sender: Any) { //BGMの停止 backmusicPlayer.stop() } }メソッド定義部分
fileprivate func soundPlayer(player:inout AVAudioPlayer,path:URL,count: Int)fileprivate・・・アクセス修飾詞といって、このファイルの中でのみ呼び出せるメソッドになる。
player:inout AVAudioPlayer・・・AVAudioPlayerクラスの変数を、受け取って、最終的にこの変数を戻り値として返すよ、という意味。returnを明記する必要がなくなる。呼び出し部分
@IBAction func play(_ sender: Any) { soundPlayer(player: &backmusicPlayer, path: backmusicPath, count: -1) }soundPlayerにそれぞれ3つの引数をわたしている。
左からインスタンス、音源ファイルのパス、そして繰り返す回数である。(-1はループになる)参照渡し
playerという引数は、参照渡しという方法でメソッド側に値が渡されている。
呼び出し元の変数がメソッドの影響を受けるとき、参照渡しが行われる。参照渡しと値渡しの例
http://swift-salaryman.com/inout.php
より引用import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() var testInt = 0; println(testInt);//結果ー>0 sanshouWatashi_NO(testInt); println(testInt);//結果ー>0 sanshouWatashi_YES(&testInt); println(testInt);//結果ー>1 } //参照渡しではない通常(呼び出し元影響受けない) func sanshouWatashi_NO(var param1:Int){ param1 += 1; } //参照渡し(呼び出し元影響受ける) func sanshouWatashi_YES(inout param1:Int){ param1 += 1; } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }cf.AVAudioPlayer
https://developer.apple.com/documentation/avfoundation/avaudioplayer
AVAudioPlayerクラスで使えるメソッド等が公開されている。感想
超シンプルじゃんけんアプリと音楽再生アプリを作成してみて、なんとなく流れを掴めた。
まずはパーツを配置して、そのパーツの大きさやフォーマットを整える。
そしてそのパーツとプログラムをリンクさせる。
とりあえずまずは、この本を最後までやりたい。
- 投稿日:2020-03-28T19:39:55+09:00
初心者によるRxSwiftのデモ
RxSwiftとは
RxSwiftの前身はMicrosoftが開発した.NET用のライブラリ「Reactive Extensions」です。このReactive Extensionsの概念が有効だったため、数々のプログラミング言語へと移植されました。その中でSwiftに移植されたのがRxSwiftです。
リアクティブプログラミングとは
リアクティブプログラミングとは「値の変化」と、それに対する「振る舞い」の関係を宣言的に記述するプログラミングの一種です。
例えばボタンを押すとラベルの文字が変化する、のようなユーザーインタラクティブなシステムや、通信処理などの非同期処理などで簡潔にかけるため特に有用です。数々の言語へ移植されている点がその有用性の証明とも言えます。SwiftではiOS13専用で公式よりCombineというフレームワークがリリースされましたが、これはRxSwiftの概念を多く真似して作られているようです。
Combineを理解するための前提として、また簡潔なコードを書くためRxSwiftを始めてみようと思います。
参考書籍: 比較して学ぶRxSwift入門
カウンターデモアプリ
以下のように、ボタンを押すとラベルの数字が増減、または0にリセットされるデモアプリを作ってみようと思います。
https://github.com/Satoru-PriChan/RxSwiftCounterApp
コールバックで実装
比較対象としてコールバックで実装してみます。
CounterViewModel.swiftimport Foundation class CounterViewModel { private(set) var count = 0 func incrementCount(callback: (Int) -> ()) { count += 1 callback(count) } func decrementCount(callback: (Int) -> ()) { count -= 1 callback(count) } func resetCount(callback: (Int) -> ()) { count = 0 callback(count) } }ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet private weak var countLabel: UILabel! private var viewModel: CounterViewModel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. viewModel = CounterViewModel() } @IBAction func countUp(_ sender: UIButton) { viewModel.incrementCount(callback: { [weak self] count in self?.updateCountLabel(count) }) } @IBAction func countDown(_ sender: UIButton) { viewModel.decrementCount(callback: { [weak self] count in self?.updateCountLabel(count) }) } @IBAction func reset(_ sender: UIButton) { viewModel.resetCount(callback: { [weak self] count in self?.updateCountLabel(count) }) } private func updateCountLabel(_ count: Int) { countLabel.text = String(count) } }ボタンに対応するメソッドが一つずつある(IBAction)ので、現段階ではシンプルですが、ボタンが増えた場合は見にくくなります。
また、
CounterViewModel
の側で、イベント処理の時にcallback
を一々呼ばなくてはなりません。デリゲートパターンで実装
CounterPresenter.swiftprotocol CounterDelegate: class { func updateCount(count: Int) } class CounterPresenter { private var count = 0 { didSet { delegate?.updateCount(count: count) } } private var delegate: CounterDelegate? func attachView(_ delegate: CounterDelegate) { self.delegate = delegate } func detachView() { self.delegate = nil } func incrementCount() { count += 1 } func decrementCount() { count -= 1 } func resetCount() { count = 0 } }ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet private weak var countLabel: UILabel! private let presenter = CounterPresenter() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. presenter.attachView(self) } @IBAction func countUp(_ sender: UIButton) { presenter.incrementCount() } @IBAction func countDown(_ sender: UIButton) { presenter.decrementCount() } @IBAction func reset(_ sender: UIButton) { presenter.resetCount() } } extension ViewController: CounterDelegate { func updateCount(count: Int) { countLabel.text = String(count) } }コールバックを呼び出す手間は無くなりました。
ボタン一つに対しIBAction一つが対応している点はコールバックと同じです。
RxSwiftで実装
CounterViewModel.swiftimport Foundation import RxSwift import RxCocoa struct CounterViewModelInput { let countUpButton: Observable<Void> let countDownButton: Observable<Void> let countResetButton: Observable<Void> } protocol CounterViewModelOutput { var counterText: Driver<String?> { get } } protocol CounterViewModelType { var outputs: CounterViewModelOutput? { get } func setup(input: CounterViewModelInput) } class CounterRxViewModel: CounterViewModelType { var outputs: CounterViewModelOutput? private let countRelay = BehaviorRelay<Int>(value: 0) private let initialCount = 0 private let disposeBag = DisposeBag() init() { self.outputs = self resetCount() } func setup(input: CounterViewModelInput) { input.countUpButton .subscribe(onNext: { [weak self] in self?.incrementCount() }) .disposed(by: disposeBag) input.countDownButton .subscribe(onNext: { [weak self] in self?.decrementCount() }) .disposed(by: disposeBag) input.countResetButton .subscribe(onNext: { [weak self] in self?.resetCount() }) .disposed(by: disposeBag) } private func incrementCount() { let count = countRelay.value + 1 countRelay.accept(count) } private func decrementCount() { let count = countRelay.value - 1 countRelay.accept(count) } private func resetCount() { countRelay.accept(initialCount) } } extension CounterRxViewModel: CounterViewModelOutput { var counterText: Driver<String?> { return countRelay .map { "Rx pattern: \($0)" } .asDriver(onErrorJustReturn: nil) } }ViewController.swiftimport UIKit import RxSwift class ViewController: UIViewController { @IBOutlet private weak var countLabel: UILabel! @IBOutlet weak var countUpButton: UIButton! @IBOutlet weak var countDownButton: UIButton! @IBOutlet weak var resetButton: UIButton! private let disposeBag = DisposeBag() private var viewModel: CounterRxViewModel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. setupViewModel() } func setupViewModel() { viewModel = CounterRxViewModel() let input = CounterViewModelInput(countUpButton: countUpButton.rx.tap.asObservable(), countDownButton: countDownButton.rx.tap.asObservable(), countResetButton: resetButton.rx.tap.asObservable()) viewModel.setup(input: input) viewModel.outputs?.counterText .drive(countLabel.rx.text) .disposed(by: disposeBag) } }ViewControllerにおいて、ボタンと反応するメソッドが一対一では無くなり、見やすくなっています。
ViewModelにおいて、
delegate?.updateCount
のようにデータ更新の通知を行わなくても良くなっています。イベントが発生した場合、自動で通知が流れていきます。比較的記述量が多い問題があります。極めて規模の小さいプロジェクトには向かないかもしれません。
WebViewアプリ
次はプログレスバー、アクティビティインジケータ(グルグル回る物)を備えたブラウザアプリを実装します。
https://github.com/Satoru-PriChan/RxSwiftWKWebViewDemo
通常のKVOパターンと、RxSwiftを使って実装し比較します。
KVOパターンで実装
KVOとはkey value observingの略で、プロパティの値の変化を監視する仕組みのことを言います(参照 https://qiita.com/ObuchiYuki/items/d00ce5f44725672184da)
ViewController.swiftimport UIKit import WebKit class ViewController: UIViewController { @IBOutlet weak var webView: WKWebView! @IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! private var observers = [NSKeyValueObservation]() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. observers.append(webView.observe(\.isLoading, options: .new, changeHandler: { [weak self] (webView, change) in //observer webView's estimatedProgress guard change.newValue != nil else { return } if change.newValue ?? false { //when loading if self?.activityIndicator.isAnimating == false { self?.activityIndicator.startAnimating() } } else { self?.activityIndicator.stopAnimating() //when load completed, set progress 0.0 in progressView. self?.progressView.setProgress(0.0, animated: false) //when load is completed, set title of loaded page as NavigationTitle. self?.title = webView.title } })) observers.append(webView.observe(\.estimatedProgress, options: .new, changeHandler: {[weak self] (_, change) in guard change.newValue != nil else { return } self?.progressView.setProgress(Float(change.newValue!), animated: true) })) } override func viewWillAppear(_ animated: Bool) { loadWebView() } private func loadWebView() { let url = URL(string: "https://www.google.com/") let urlRequest = URLRequest(url: url!) webView.load(urlRequest) progressView.setProgress(0.1, animated: true) } }
observe
メソッド内で、値の変化時に必要な処理を全て書かなければいけないので、少し読みづらくなっています。RXSwift + RxWebKitで実装
ViewController.swiftimport UIKit import WebKit import RxSwift import RxCocoa import RxOptional import RxWebKit class ViewController: UIViewController { @IBOutlet weak var webView: WKWebView! @IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. setupWebView() } private func setupWebView() { //define observer let loadingObservable = webView.rx.loading .share() //display or hide progress bar loadingObservable .map { return !$0 } .observeOn(MainScheduler.instance) .bind(to: progressView.rx.isHidden) .disposed(by: disposeBag) //activity indicator loadingObservable.subscribe(onNext: {[weak self] (isLoading) in if isLoading { //start animating if self?.activityIndicator.isAnimating == false { self?.activityIndicator.startAnimating() } } else { self?.activityIndicator.stopAnimating() } }).disposed(by: disposeBag) //navigationbar title loadingObservable .map { [weak self] _ in return self?.webView.title } .bind(to: navigationItem.rx.title) .disposed(by: disposeBag) //progress bar webView.rx.estimatedProgress .map { return Float($0) } .observeOn(MainScheduler.instance) .bind(to: progressView.rx.progress) .disposed(by: disposeBag) } override func viewWillAppear(_ animated: Bool) { loadWebView() } private func loadWebView() { let url = URL(string: "https://www.google.com/") let urlRequest = URLRequest(url: url!) webView.load(urlRequest) progressView.setProgress(0.1, animated: true) } }ネストが浅くなり、読みやすくなりました。
- 投稿日:2020-03-28T19:08:03+09:00
iOSアプリのMultitasking対応について
はじめに
WWDC2019にて2020年4月までにMultitaskingをサポートすることが必須と発表されました。
そして今年1月、必須ではないにしてもMultitaskingをサポートすることを 強くお勧めする とアナウンスがありました。
もちろん対応しなければアプリをリジェクトされるわけではありませんが、今後Multitaskingが重要となっていくことは明らかです。
そこで今回はMultitaskingの対応方法および、注意すべき点についてまとめていきます。
iPhoneとiPadに適したユーザーインターフェイスの構築Mutitasking設定方法
Requires full screen
にチェックが入っている場合は外しますDevice Orientation
を4つ全ての方向に対応するようにチェックします- もし、iPadのみで全ての方向に対応するようにする場合は
General
のDevice Orientation
は弄らず、info.plist
からSupported interface orientations (iPad)
にて4つの方向全て対応するよう設定して下さい- 最後に、もし起動画面をStoryboardではなくimageで作成している場合は
Launch Screen File
にてLaunch用のStoryboard
を作成して設定します- あとはiPadでビルドすればMutliTaskingに対応していることを確認することができます
Mutitasking対応方法
Mutitaskingの対応すると今までにはなかったSplit表示になる(画面分割される)タイミングなどでレイアウトを更新する必要が出てきます。
AutoLayoutにレイアウトの計算を全て任せることができていれば大きく対応する必要はありませんが、手動で計算をしている箇所等についてはこちらで意図的に再計算を走らせる必要があります。
もちろん、レイアウトを手動計算する方法で対応するよりもAutolayoutにて対応するのが理想ではあるとは思いますが、そう簡単に対応できるわけではありませんので、手動でのレイアウトが必要な場合の対応方法について記載します。レイアウト再計算処理タイミングの見直し
ViewControllerのLifececle関数にてMultitaskingによる画面サイズの変更を検知してくれるのは大きく以下の2つです。
viewWillTransition(to:with:)
Multitasking(および画面回転)による画面サイズの変更は
viewWillTransition(to:with:)
にて検知できます。
また、サイズ変更後のアプリのWindowサイズは引数のsize
の中に格納されています。/// 画面回転時、およびMultitaskingによる画面サイズ変更時に呼ばれる override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in // 遷移アニメーション時に行う処理を記述 }, completion: { _ in // 遷移終了後に行う処理を記述 }) }viewDidLayoutSubviews()
viewDidLayoutSubviews
でも画面サイズの変更を検知することができます。
ただし、こちらはMultitaskingによる画面サイズ変更のみでなく、レイアウトが更新されるタイミング全てで呼ばれますので、コストがかなりかかってしまう可能性があります。
使用する場合は慎重に決めて下さい。UIScreen.main.boundsについて
UIScreen.main.bounds
、幅や高さ計算で何かと便利で使ってしまいがちですが、これを使用してframe計算をしている箇所についてはほぼ全て修正する必要があります。
UIScreen.main.bounds
は、あくまで 端末のサイズ を取得するモノで、 アプリの表示サイズ を取得するモノではありません。
SplitViewで表示される際はアプリの表示サイズが端末サイズよりも小さい状態で表示される場合がありますので、UIScreen.main.bounds
を使用しているとレイアウトが崩れる場合があります。
その場合は下記の方法にて、現在表示されているアプリサイズ(Window)のboundsをとってくることができますので、必要に応じてUIScreen.main.bounds
の箇所を書き換えることで対応できます。/// アプリの現在表示されているWindow(KeyWindow)のサイズを取得してくる UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.bounds /// 下記でも同様の結果を取得できるが、keyWindowがiOS13にてdisabledとなっているので使用しない UIApplication.shared.keyWindow?.boundsおわりに
以上で基本的にはMutltaskingに対応することができると思います。
対応自体は比較的単純なのですが、規模が大きいアプリはカバーする画面数が多く大変になりがちです。
しかし、今後Multitaskingの重要性も増していくことと思いますので、是非この機会に対応してみてはいかがでしょうか?参考文献