20210321のSwiftに関する記事は10件です。

ナビゲーションバーのカスタマイズ一覧(iOS)

はじめに

ナビゲーションバーをカスタマイズする方法を毎回忘れて実装に時間がかかるため、まとめました。

環境

  • OS:macOS Big Sur 11.1
  • Xcode:12.4 (12D4e)
  • Swift:5.3.2
  • iOS:14.4

ナビゲーションバーのカスタマイズ一覧

ナビゲーションバーのタイトルに画像をセットする

UINavigationItem.titleView に画像をセットすることで、ナビゲーションバーのタイトルを画像にできます。

MonsterIndexListViewController.swift
final class MonsterIndexListViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let imageView = UIImageView(image: UIImage(named: "navi_zukan"))
        imageView.contentMode = .scaleAspectFit
        self.navigationItem.titleView = imageView
    }

}
テキスト 画像
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 20.32.26.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 20.30.14.png

UIImageView.contentMode.scaleAspectFit にすると、画像の縦横比を変えずに表示できます。

ナビゲーションバーの戻るボタンの画像を変更する

UINavigationBarbackIndicatorImagebackIndicatorTransitionMaskImage を変更することで、戻るボタンの画像を変更できます。

MonsterIndexListViewController.swift
final class MonsterIndexListViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let image = UIImage(named: "index_icon_back")
        self.navigationController?.navigationBar.backIndicatorImage = image
        self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = image
    }

}
画像未変更 画像変更
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 19.09.43.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 19.34.20.png

セットした画像は以下であり、 tintColor が反映されてしまいます。
icon_back.png

tintColor を反映させない方法がわからなかったので、ご存じの方がいたら教えていただけると嬉しいです。

2021/03/23, 追記
コメント で教えていただきました。

対象画像のRender Asを Default から Original Image に変えると tintColor が反映されなくなります。
スクリーンショット_2021-03-23_19_40_17_after.png
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-23 at 19.39.01.png

ただ画像の位置が上に寄るのは直りません。
画像のサイズを調整すればかんたんに対応できます。
参考: https://sarunw.com/posts/how-to-change-back-button-image/#position

UIImage の位置を変える方法もありますが、 tintColor が反映されるのと、 UIImageView の位置は変わらないので見切れることがあるため、ベターな方法ではなさそうです。
参考: https://stackoverflow.com/questions/29445644/vertically-center-backindicatorimage-in-swift

ナビゲーションバーの戻るボタンのテキストを非表示にする

iOS 14以上の場合、 遷移元のビューコントローラー
UINavigationItem.backButtonDisplayMode.minimal にすることで、戻るボタンのテキストを非表示にできます。

MainViewController.swift
final class MainViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.navigationItem.backButtonDisplayMode = .minimal
    }

}
表示 非表示
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 19.09.03.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 19.09.43.png

戻るボタンの長押し時にビューコントローラーの title が表示されるので、戻るボタンを非表示にする場合でもセットするのが望ましいです。

MainViewController.swift
final class MainViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

+       self.title = "メイン"
        self.navigationItem.backButtonDisplayMode = .minimal
    }

}
タイトル未セット タイトルセット
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 19.17.05.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 19.11.50.png

戻るボタンのタイトルに半角スペースをセットする方法もよく見ますが、iOS 14以降では戻るボタン長押し時のメニューにタイトルが表示されなくなるので望ましくありません。

ナビゲーションバーの戻るボタンの色を変更する

UINavigationBartintColor を変更することで、戻るボタンの色を変更できます。

MonsterIndexListViewController.swift
final class MonsterIndexListViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.navigationController?.navigationBar.tintColor = UIColor(named: "tekimon_khaki")
    }

}
色未変更 色変更
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-23 at 20.18.10.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-23 at 20.21.45.png

一部のビューコントローラーのみナビゲーションバーの戻るボタンの色を変更する場合、戻し忘れに気をつけてください。

戻すには nil をセットします。

MainController.swift
final class MainViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.navigationController?.navigationBar.tintColor = nil
    }

}

ナビゲーションバーの背景色を変更する

UINavigationBarbarTintColor を変更することで、ナビゲーションバーの背景色を変更できます。

MonsterIndexListViewController.swift
final class MonsterIndexListViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.navigationController?.navigationBar.barTintColor = UIColor(named: "tekimon_khaki")
    }

}
背景色未変更 背景色変更
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-23 at 21.01.22.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-23 at 20.59.02.png

一部のビューコントローラーのみナビゲーションバーの背景色を変更する場合、戻し忘れに気をつけてください。

戻すには nil をセットします。

MainController.swift
final class MainViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.navigationController?.navigationBar.barTintColor = nil
    }

}

ナビゲーションバーを透過する

UINavigationBarbackgroundImageshadowImage を初期化することで、ナビゲーションバーを透過できます。

MonsterIndexListViewController.swift
final class MonsterIndexListViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
        self.navigationController?.navigationBar.shadowImage = UIImage()
    }

}
非透過 透過
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 20.39.05.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 20.36.22.png

一部のビューコントローラーのみナビゲーションバーを透過する場合、戻し忘れに気をつけてください。

非透過にするには nil をセットします。

MainViewController.swift
final class MainViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        self.navigationController?.navigationBar.shadowImage = nil
    }

}

私が参考にしたサイトでは、 UINavigationBar に以下のような拡張メソッドを実装していました。
意図が明確になっていいと思います。

UINavigationBar+Transparency.swift
extension UINavigationBar {

    func enableTransparency() {
        setBackgroundImage(UIImage(), for: .default)
        self.shadowImage = UIImage()
    }

    func disableTransparency() {
        setBackgroundImage(nil, for: .default)
        self.shadowImage = nil
    }

}

ナビゲーションバーを非表示にする

UINavigationController.setNavigationBarHidden(_:animated:) メソッドを呼び出すことで、ナビゲーションバーの表示/非表示を切り替えられます。

MainViewController.swift
final class MainViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.navigationController?.setNavigationBarHidden(true, animated: false)
    }

}
表示 非表示
Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 18.38.41.png Simulator Screen Shot - iPhone SE (2nd generation) - 2021-03-21 at 18.35.47.png

ナビゲーションバーの表示/非表示が混在しているアプリの場合、画面を遷移するたびにナビゲーションバーの表示も切り替える必要があるので、 viewWillAppear(_:) メソッド内で実行すべきです。

ナビゲーションバーを表示 or 隠し忘れないように気をつけてください。

おまけ: スクリーンショットのアプリ

スクリーンショットに使ったアプリは、先日行われたHack Day 2021で私たちのチームが開発しました!

詳細は @hcrane14 さんがまとめているので、よかったら見てください。
https://note.com/hcrane/n/n98a8f1157390

おわりに

これでナビゲーションバーをカスタマイズする案件が来ても安心です :relaxed:

self.navigationController を変更すると他のビューコントローラーにも影響するので viewWillAppear(_:) で行って戻すのを忘れない、 self.navigationItem は変更しても他のビューコントローラーに影響しないので viewDidLoad() で行う、と覚えておくといいです(間違っていたらすみません)。

参考リンク

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

TabeleView sectionの不要のスペースを除去

sectionFooterHeight と sectionHeaderHeight を0にするのと、
tableHeaderView, tableFooterView のそれぞれのviewにdummyのviewを代入するとよさそう。
dummyのviewは高さを0にしてしまうと不要スペースが消えないため注意

ViewController.swift
import UIKit

class ViewController: UIViewController {

    let tableView: UITableView = UITableView(frame: .zero, style: .grouped)
    let list: [String] = ["あ", "い", "う", "え"]

    override func viewDidLoad() {
        super.viewDidLoad()
        configureLayout()
    }

    private func configureLayout() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.sectionHeaderHeight = 0 // 今回の場合、delegateで高さ指定しているため不要
        tableView.sectionFooterHeight = 0
        let dummyView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: .zero, height: 0.01))) // 0にすると高さが反映されない
        tableView.tableHeaderView = dummyView // 今回の場合、delegateで高さ指定しているため不要
        tableView.tableFooterView = dummyView

        view.addSubview(self.tableView)

        self.tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
    }
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {

    // MARK: tableView rows
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 2:
            return 0
        default:
            return list.count
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = list[indexPath.row]
        return cell
    }

    // MARK: tableView Section
    // セクション数
    func numberOfSections(in tableView: UITableView) -> Int {
        return 5
    }

     // セクションタイトル
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "SectionTitle " + String(section)
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let rect = CGRect.init(origin: .zero, size: CGSize(width: tableView.bounds.width, height: .zero))
        let view = UIView(frame: rect)

        switch (section % 2) {
        case 0:
            view.backgroundColor = .blue
        default:
            view.backgroundColor = .green
        }

        return view
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        switch section {
        case 2:
            return 0
        default:
            return 50
        }
    }
}

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

【審査通りました!】【Swift UI編】初心者がApp StoreにiOS自作アプリを公開する過程

審査通りました!(2021/3/20)

ここからダウンロードページに飛べます

output.gif

なぜ書くのか

初めて作るアプリのリリース過程を残すため。

SwiftUIがめちゃめちゃ書きやすい

なぜstoryboardからSwiftUIに切り替えたのか

私はアプリ開発全くの初心者なのですが、先月までSwiftのstoryboardを使ってアプリ開発を行っていました。

【storyboard編:一旦頓挫】初心者がApp StoreにiOS自作アプリを公開する過程
【storyboard編:一旦頓挫】初心者が自作iOSアプリ審査に挑戦中

SwiftUIの存在は知っていたのですが、「まだ新しい技術だから、フレームワーク依存のエラーが出がち」とか「情報が少ないからstoryboardでの学習がおすすめ」とか「現場ではstoryboardがまだまだ使われてる」等々、ググって仕入れた情報からstoryboardを選択しアプリ開発をしていました。

しかし、protocol?delegate?optional型?何それ的な感じに陥っていましたし、何よりUIを作るときのAuto rayoutがすごくめんどくさかったです。おそらくこれは、独学の初心者には到底扱えない、学習にめちゃめちゃ時間がかかるんだろうなと実感していました。
それでも、とにかくやってみようと手を動かしてアプリを作ってみましたが、Appleさんにこんなクソアプリリースさせねーよと言われてしまい、一から勉強し直そうと考えました。

プログラミング言語は進化のスピードが速いということで、書籍より動画の方がより最新の状況を反映しているだろうとの予想で「udemy」でひとつ動画を買って学習していましたが、全体をさらっとみるのには時間がかかりすぎ、あまり僕の学習スタイルにはあっていないとわかりました。元々僕は本をペラペラめくりながら勉強する方が好きなので、書籍での勉強に切り替えようと思いました。

書籍を選ぶ中でやはり気になるのはどれだけ新しいか、最新のxcodeのバージョンに対応しているかというところでした。その基準で本を選んでいるとやはりSwiftUIの書籍が新しく、最新のxcodeのバーションにも対応しているようでしたので、SwiftUIに切り替えました。

買った本たち

SwiftUI対応 たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 12/iOS 14対応
詳細! SwiftUI iPhoneアプリ開発入門ノート[2020] iOS 14+Xcode 12対応
[増補改訂第3版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus

めちゃくちゃ書きやすい

SwiftUI対応 たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 12/iOS 14対応をとりあえず一周(15時間くらいでサラッと)したあとにアプリを作り始めました。そうするとstoryboardよりも体感20倍くらいの速さでUIができて、感動しました。プレービュー機能のおかげでシミュレータを立ち上げる必要なく見た目の変化がわかるのがスピードが上がった要因かと思います。

UIを整えた後に機能を追加していきました。SwiftUI対応 たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 12/iOS 14対応を振り返りながら、詳細! SwiftUI iPhoneアプリ開発入門ノート[2020] iOS 14+Xcode 12対応を参考書がわりにしながら([増補改訂第3版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plusはこれまでほとんど使っていません)、ググりながらやることで2週間程度で当初予定した機能を含むアプリができました。

iOSアプリ審査

審査1回目(2021/3/19)

storyboardでのアプリ制作のときにすでにアプリの審査提出は経験済みなので、今回も同じ手順で行いました。審査提出から1日以内にレビュワーさんからリジェクトの通知が来ました。今回は2つのリジェクト理由がありました。

Guideline 2.1 - Information Needed

最初に審査に提出したアプリの学習画面の右上にスターアイコンを設置していました。これは次のアップデートでお気に入り機能を追加しようと考えていたためです。そこにレビュワーさんの指摘をもらい、「どこでお気に入りリストが見れるの?」と聞かれました。
次のアップデートでお気に入り機能は追加予定なので、スターアイコンは一旦消しますと返信しました。

名称未設定 1.png

Guideline 2.3.3 - Performance - Accurate Metadata

こちらは審査提出時のスクリーンショットについてiPadサイズのスクリーンショットがiPhoneUIになってるよという指摘でした。そもそも僕のアプリはiPadようには作っていなかったので、最初の審査提出ではなんでもいいのかなと思っていました。指摘もらいましたので、xcodeでシミュレータを起動しiPad用のサイズのスクリーンショットを提出しました。

審査2回目(2021/3/20)

審査通りました!storyboradも含めて4回目でやっと通りました!

スクリーンショット 2021-03-21 15.01.53.png

まとめ

最初アプリを提出したときに絶望的なリジェクト理由(Minimum Functionality)を提示され、途方に暮れていました。
しかし、一から勉強し直すきっかけにもなり、Swiftの書き方にも慣れることができたので良い経験になったと思います。

このアプリについて、今のところ思い付いているやりたいことを以下に書いておきます。
・お気に入り機能追加
・プレイリスト機能追加(イメージはiPhoneのミュージックアプリ)
・例文リクエスト機能(ユーザーが追加して欲しい例文を僕に送ることができる機能)

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

【Swift】キーボードに合わせてViewをアニメーションさせる

キーボードに合わせてUITextFieldやUIViewをアニメーションさせたい時に、animate(withDuration:animations:completion:)でアニメーションさせようと思ったら上手く動かなかったので調べてみました。
UIResponderを使うと上手くいったので、困っている人は参考にしてみて下さい!

サンプルコード(GitHub)

こんな感じのが作れます。

キーボードの表示・非表示を検知する

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}

// Keybordが表示
@objc dynamic func keyboardWillShow(_ notification: NSNotification){

}

// Keybordが非表示
@objc dynamic func keyboardWillHide(_ notification: NSNotification){

}

キーボードのアニメーションを抽出・アニーメーションを実行

@objc dynamic func keyboardWillShow(_ notification: NSNotification){
    // キーボードのdurationを抽出 *1
    let durationKey = UIResponder.keyboardAnimationDurationUserInfoKey
    let duration = notification.userInfo![durationKey] as! Double

    // キーボードのframeを抽出する *2
    let frameKey = UIResponder.keyboardFrameEndUserInfoKey
    let keyboardFrameValue = notification.userInfo![frameKey] as! NSValue

    // アニメーション曲線を抽出する *3
    let curveKey = UIResponder.keyboardAnimationCurveUserInfoKey
    let curveValue = notification.userInfo![curveKey] as! Int
    let curve = UIView.AnimationCurve(rawValue: curveValue)!

    let animator = UIViewPropertyAnimator(duration: duration, curve: curve) {
        // ここにアニメーション化したいレイアウト変更を記述する
        self.view?.layoutIfNeeded()
    }

    animator.startAnimation()
}

*1 キーボードのduration
*2 キーボードのframe
*3 アニメーション曲線

レイアウト設定時の注意

textFieldなどをキーボードの上にくっつけて表示したい場合はレイアウトの設定をSafe Areaに合わせるとSafe Area分の空白はキーボードとViewの間にできてしまうので注意です。
こんな感じでSuper Viewに合わせましょう!
スクリーンショット 2021-03-21 15.32.52.png

これをUIViewControlleなどでextensionして共通で使えるようにするのがおすすめです。

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

TCAでもUseCase/Repository/DataSourceが使いたい

はじめに

この内容は『iOSアプリ開発のためのFunctional Architecture情報共有会4』のための資料です。


導入

The Composable Architecture (TCA)では副作用実行にグローバルな関数1やClient、またはManagerというのが使われる。公式サンプルだけでなくpointfreeのゲームアプリのコードでもClientがよく出てくる。これを普段よく使われるUseCase/Repository/DataSourceを使いたいというのが発表の主旨。

名称未設定.004.png


TCAの副作用実行のための関数やClientについてのおさらい


(グローバルな)関数?

サンプルではグローバルな関数を使っていた。

次のコードはWeb API呼び出しで任意の数字からその数字にまつわるトリビアを返す非同期な処理。

func liveNumberFact(for n: Int) -> Effect<String, NumbersApiError> {
  return URLSession.shared.dataTaskPublisher(for: URL(string: "http://numbersapi.com/\(n)/trivia")!)
    .map { data, _ in String(decoding: data, as: UTF8.self) }
    .catch { _ in
      // Sometimes numbersapi.com can be flakey, so if it ever fails we will just
      // default to a mock response.
      Just("\(n) is a good number Brent")
        .delay(for: 1, scheduler: DispatchQueue.main)
    }
    .setFailureType(to: NumbersApiError.self)
    .eraseToEffect()
}

関数利用側でEnvironmentによってDIする場合。Swiftにおいても、関数も変数として扱えるのでDIするときは(Parameter) -> Effectクロージャさえ合えばいい。

struct EffectsBasicsEnvironment {
  var numberFact: (Int) -> Effect<String, NumbersApiError>
}

これで結果を置き換えられるのでXcodeプレビューでも楽だし、テストコードも楽に書ける。

ただ、グローバルな関数として用意するのは複数人でアプリを作るときはあまりやりたくない。サンプルだから良いけども。


Client/Managerとは?

公式のサンプルではClient/Managerは次の通り。

  • Client系
    • WebSocketClient
    • SpeechClient
    • DownloadClient
    • AudioPlayerClient
    • WeatherClient
    • AudioRecorderClient
  • Manager系
    • LocationManager(これはサンプルというよりTCA公式のLocationラッパーかも)
    • MotionManager(これはサンプルというよりTCA公式のMotionラッパーかも)

ちなみにClientってどんなものなんだろう?


Clientにはvarクロージャが処理を返しロジックが入れ替えられるようになってる。

struct WeatherClient {
  var searchLocation: (String) -> Effect<[Location], Failure>
  var weather: (Int) -> Effect<LocationWeather, Failure>

  struct Failure: Error, Equatable {}
}
struct SearchEnvironment {
  var weatherClient: WeatherClient
  var mainQueue: AnySchedulerOf<DispatchQueue>
}

クロージャを変数で入れる場合にEnvironmentに直接入れるのではなく、型としてまとめている。


プロダクション用の実処理自体はextensionに.live定数で用意

extension WeatherClient {
  static let live = WeatherClient( // 初期化してクロージャに処理を用意してそれを定数 .live として返す
    searchLocation: { query in
      var components = URLComponents(string: "https://www.metaweather.com/api/location/search")!
      components.queryItems = [URLQueryItem(name: "query", value: query)]

      return URLSession.shared.dataTaskPublisher(for: components.url!) // URLSessionをDIしてない(する気がない
        .map { data, _ in data }
        .decode(type: [Location].self, decoder: jsonDecoder)
        .mapError { _ in Failure() }
        .eraseToEffect()
    },
    ...

短所

  • 処理がstaticな定数だから変数を参照することはできない
    • URLSessionをDIすることができない
      • WeatherClient自体を置き換えやすいが、WeatherClient自体をテストする気がない
        • 例えばDBで特定の要素が上書きされるロジックのテストとかはやる気がない

おそらくReducerの仕様さえテストできてれば良くて、そこから奥にあるミスっていうのはReducerの仕様に関するテストコードで気付こうという感じなのではないか。


短所を解決するには?

呼び出し時に引数で置き換えたいものを渡す

  // Reducer内
  case let .locationTapped(location):
    struct SearchWeatherId: Hashable {}

    state.locationWeatherRequestInFlight = location

    return environment.weatherClient
      .weather(location.id, /* ここでURLSessionを渡す */)

もしくは定数.liveのなかでFactory.getURLSession()みたいなのを用意してプリプロセッサ的なマクロで切り替えて取り出してもいい(が、その仕組みを自動化せず作るのは自分はあんまり...)。


ここまでのまとめ

  • pointfreeの人たちは細かな副作用の単体テストをReducerのテストでカバーしてる
    • 副作用での細かな処理のテストをやるには引数で渡すとか、Factory的なのを内部で切り替える

おまけ: 最近公開されたpointfreeのゲームのコードもClient

  • DictionaryClient
  • FeedbackGeneratorClient
  • FileClient
  • LocalDatabaseClient
  • LowPowerModeClient
  • RemoteNotificationsClient
  • ServerConfigClient
  • UIApplicationClient
  • UserDefaultsClient

Managerはない。


UseCase/Repository/DataSourceで副作用を呼び出す

Google I/O 2019 - 2021 Android Appから

image.png


層の解説

  • ドメイン層(いわくlightweight domain layer)
    • データレイヤーとプレゼンテーションレイヤーの間に位置
      • UIスレッドから離れた場所でビジネスロジックの個別部分を処理
    • UseCaseクラスを中心に構成されてる
    • コールバック地獄を避けるために、LiveData を使用してユースケースの結果を公開
      • 2020あたりではコルーチンも使われはじめる
      • 2018あたりまではexecuteNowメソッドで同期的なものは直接返したり
        • そもそもAndroid Blue Printという設計のお手本リポジトリがあった
  • データ層
    • リポジトリモジュールは、すべてのデータ操作を処理し、アプリの他の部分からデータソースを抽象化する役割を担う

データ層についての引用

私たちはFirestoreを気に入って使用していましたが、将来的に別のデータソースに交換したくなった場合、このアーキテクチャによってすっきりとした方法で交換することができます)


Google I/O 2019 - 2021 Android AppにおけるUseCase実装

...

/**
 * A [UseCase] that returns the [UserSession]s for a user.
 */
class LoadUserSessionOneShotUseCase @Inject constructor(
    private val userEventRepository: DefaultSessionAndUserEventRepository,
    @IoDispatcher dispatcher: CoroutineDispatcher
) : UseCase<Pair<String, SessionId>, UserSession>(dispatcher) {
    // suspendさせてコルーチンなメソッドだが
    override suspend fun execute(parameters: Pair<String, SessionId>): UserSession {
        val (userId, eventId) = parameters
        // 実処理は非同期ではなく同期処理
        return userEventRepository.getUserSession(userId, eventId)
    }
}
  • セッション情報を取得するUseCase
    • コルーチンで結果UserSessionを返してる
    • 処理はuserEventRepositorygetUserSessionメソッド
      • 同期も非同期もインタフェース揃えるためsuspend funでコルーチンだけど、同期的に処理2

SwiftでUseCaseを作る場合を考える


同期処理するSyncUseCaseの例

public protocol SyncUseCase where Failure: Error {
    associatedtype Parameters
    associatedtype Success
    associatedtype Failure

    func perform(_ parameters: Parameters) -> Result<Success, Failure>
}

このSyncUseCaseを直接利用する型でそのまま準拠してもそのまま使うことはできないので型消去するがその話は別に書いてる


ProfileをSaveするUseCaseを例に

public class SaveProfileUseCase: SyncUseCase {
    public typealias Parameters = Profile
    public typealias Success = ()
    public typealias Failure = Error

    private let dataSource: ProfileLocalDataSource

    init(dataSource: ProfileLocalDataSource) {
        self.dataSource = dataSource // initでdataSourceを入れ替えられるように
    }

    public func perform(_ parameters: Parameters) -> Result<Success, Failure> {
        dataSource.save(Profiles: parameters) // 実処理はdataSourceのCore Dataとかでやってる
    }
}

import ComposableArchitecture

extension SaveProfileListUseCase { 
    static func executeEffect(Profile: Parameters) -> Effect<Success, Failure> {
        .result { perform(profile) } // Effectにして結果を返す
    }
}

UseCase型の長所と短所

  • 長所
    • initでDataSourceを入れるので実行時に引数で用意するわけじゃない
      • 従来のプログラミングスタイルと同じ感じでできる
    • UseCase自体のテストコードを書ける(依存してるDataSourceを置き換えられるし)
  • 短所
    • 関数型のやり方に従来のオブジェクト指向型のプログラミングスタイルが混ざることの心理的抵抗感
    • UseCase型がめちゃくちゃ増える

UseCaseは使わないがRepository/DataSourceを使う


UseCase型を使わずに、しかしReducer以外の副作用の単体テストしたいのでRepository/DataSourceは使う場合を考える。

// プロフィールを変更して上書きする的なState, Action Environmentの集まり
enum ProfileSettingCore {
    enum State { ... }

    struct Action { ... }

    struct Environment {
        // DIできるようにする
        var save: (Profile, ProfileDataSource) -> Result<(), Error> 
    }

    static let reducer = ...
}

ProfileをSaveする例

// プロフィールを変更して上書きする的なState, Action Environmentの集まり
enum ProfileSettingCore {
    enum State { ... }    
    struct Action { ... }

    struct Environment {
        // DIできるようにする
        var save: (Profile, ProfileDataSource) -> Result<(), Error> 
    }

    static let reducer = ...
}

extension ProfileSettingCore {
    enum SideEffect {
        static func save(profile: Profile, dataSource: ProfileDataSource) -> Result<(), Error> {
            dataSource.update(profile) // 実質的なUseCaseの処理
        }
    }
}

UseCaseを使わないSideEffectでグルーピングされたstatic関数の長所と短所

  • 長所
    • シンプル(UseCase型の面倒な型消去なんて考えなくていい)
    • SideEffect自体のテストコードを書ける(依存してるDataSourceを置き換えられるし)
  • 短所
    • 副作用実行の引数で依存するRepository/DataSourceを入れるのはかったるい

まとめ

  • 何がベストなのかはチームにあってるやり方を採用したらいい
  • pointfree自体のやり方でClientをつくるってのもそれがチームに合っていればそれでいい

  1. もちろんグローバルである必要はなくサンプルだからグローバルな関数になってるだけだとは思う。 

  2. 他にもインタフェースはあり、invokeとかあり、むかしはexecuteNow, performなどがあった。 

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

RxSwiftを使ってMapKitで現在地から場所をサジェストするauto completeを実装するサンプル

現在地付近のの施設を検索できるサンプルになります

MapKitを使用して現在地を取得しながら、UISearchBarに入力した結果をtableViewに返すサンプルになります。
実際に使用する際は、現在地の取得の許可を得るために
info.plistに

Privacy - Location When In Use Usage Description
Privacy - Location Always and When In Use Usage Description

をかいてください。

現在地の取得が、

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])

常にアップデートされるため、

**メートル以内の変更は通知しないよう以下の関数を定義してあります。

func distinctUntilChangeGreaterThan(meters: CLLocationDistance) -> Observable<CLLocation>

渡ってきたCLLocationを
.distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000))
1000メートル以内の変化は通知しないように。

    var myLocationRelay = BehaviorRelay<CLLocation?>(value: nil)

        myLocationRelay
            .compactMap { $0 }
            .distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000))
            .subscribe(onNext: { [weak self] coordinate in
                let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
                let region = MKCoordinateRegion(center: coordinate.coordinate, span: span)
                self?.searchCompleter.region = region
            })
            .disposed(by: bag)

すべてのコード

import CoreLocation
import MapKit
import RxCocoa
import RxSwift
import UIKit

class FindLocationViewController: UIViewController {
    var viewModel: FindLocationViewModel!
    var bag = DisposeBag()

    var searchCompleter = MKLocalSearchCompleter()
    var locationManager: CLLocationManager?
    var searchResults = [MKLocalSearchCompletion]()
    var myLocationRelay = BehaviorRelay<CLLocation?>(value: nil)

    @IBOutlet private weak var searchResultsTable: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()
        initialSettings()
        viewModel.setUp()
        setBind()
        whereIsMyLocation()
    }

    private func initialSettings() {
        searchBar.delegate = self
        searchResultsTable.delegate = self
        searchResultsTable.dataSource = self
        searchCompleter.delegate = self
        let categories: [MKPointOfInterestCategory] = [.fitnessCenter]
        let filters = MKPointOfInterestFilter(including: categories)
        searchCompleter.pointOfInterestFilter = .some(filters)
    }

    private func setBind() {
        myLocationRelay
            .compactMap { $0 }
            .distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000))
            .subscribe(onNext: { [weak self] coordinate in
                let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
                let region = MKCoordinateRegion(center: coordinate.coordinate, span: span)
                self?.searchCompleter.region = region
            })
            .disposed(by: bag)
    }

    private func whereIsMyLocation() {
        locationManager = CLLocationManager()
        locationManager?.delegate = self
        locationManager?.requestWhenInUseAuthorization()

        if CLLocationManager.locationServicesEnabled() {
            locationManager!.startUpdatingLocation()
        }
    }
}

extension FindLocationViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let newLocation = locations.last else {
            return
        }

        let location: CLLocationCoordinate2D
            = CLLocationCoordinate2DMake(newLocation.coordinate.latitude, newLocation.coordinate.longitude)
        let formatter = DateFormatter()
        formatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
        let date = formatter.string(from: newLocation.timestamp)

        print("緯度:", location.latitude, "経度:", location.longitude, "時間:", date)
        let ccLoctaion = CLLocation(latitude: location.latitude, longitude: location.longitude)

        myLocationRelay.accept(ccLoctaion)
    }
}

extension FindLocationViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        let result = searchResults[indexPath.row]
        let searchRequest = MKLocalSearch.Request(completion: result)

        let search = MKLocalSearch(request: searchRequest)
        search.start { response, _ in
            guard let coordinate = response?.mapItems[0].placemark.coordinate else {
                return
            }
            guard let name = response?.mapItems[0].name else {
                return
            }

            let lat = coordinate.latitude
            let lon = coordinate.longitude

            print(lat)
            print(lon)
            print(name)
            let preVC = self.presentingViewController as! WhichGymViewController
            let gymInfo = GymInfo(coodinate: coordinate, name: name)
            preVC.viewModel.selectedGymRelay.accept(gymInfo)
            self.dismiss(animated: true, completion: nil)
        }
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return searchResults.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let searchResult = searchResults[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        cell.textLabel?.text = searchResult.title
        cell.detailTextLabel?.text = searchResult.subtitle

        return cell
    }
}

extension FindLocationViewController: MKLocalSearchCompleterDelegate & UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        searchCompleter.queryFragment = searchText
    }

    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        searchResults = completer.results
        searchResultsTable.reloadData()
    }

    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        // Error
    }

    func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
        return true
    }
}

import CoreLocation
import Foundation
import RxSwift

extension CLLocationCoordinate2D: Equatable {}

public func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
    return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}

extension ObservableType where Element == CLLocation {
    func distinctUntilChangeGreaterThan(meters: CLLocationDistance) -> Observable<CLLocation> {
        return scan(CLLocation(), accumulator: { lastLocation, location in
            if lastLocation.distance(from: location) > meters {
                return location
            } else {
                return lastLocation
            }
        }).distinctUntilChanged { lhs, rhs in
            lhs.coordinate == rhs.coordinate
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift初心者がアプリを作れるようになるまで①

開発環境

MacBook Air (2020)
macOS Big Sur Ver11.2.2
Xcode Ver12.4(12D4e)

Swiftってなに?

image.png

Swiftはappleが開発したオープンソースの言語で、iOS、Mac、Apple TV、Apple Watch向けアプリの開発に用います。

Swift。誰もが圧倒的に優れたアプリを作れる、パワフルなオープンソースの言語です。

iOS、Mac、Apple TV、Apple Watch向けのアプリを開発するためにAppleが作った、
強固で直感的なプログラミング言語。それがSwiftです。デベロッパのみなさんに、かつてないほどの
自由を届けられるように設計されています。Swiftは簡単に使えて、しかもオープンソースなので、
アイデアがある人なら誰でも、画期的なアプリを作ることができます。
(apple公式HPより引用)

おおまかに言えばiPhoneのアプリ開発ができる言語ということですね。

iPhoneアプリの開発はかつてはObject-Cがメインでしたが、Swift及びXcode(Swiftを扱えるエディタ)がリリースされてからはSwiftがメインとなっています。

理由としてはSwiftで作ったアプリは、一般的な検索アルゴリズムがこれまでよりも大幅に速く実行できるからなんです。その速さはなんとObject-Cの最大2.6倍、Python2.7の最大8.4倍と驚きの速度。(apple公式HPより引用)

ちなみにSwiftに日本語版は無いので、英語が苦手な方は翻訳を駆使して頑張ってください。

Swiftを扱うのに必要なもの

Swiftでアプリを制作するには、以下の環境が必要となってきます。
・Mac
・Xcode
・AppleID
そう、これだけです。厳密にはリリースに際してAppleディベロッパプログラムへの登録などが必要になるのですが、今回のテーマは「Swift初心者がアプリを作れるようになるまで」ということで、リリース方法はまた別で紹介しようと思います。ひとまず作るだけならMacとXcodeがあればOKです。(Xcodeが無い方はこちらからインストールしてください)

Windowsで開発できないの?

気になりますよね、これ。結論から言うと...可能です。ですが2021/3/6現在Swiftパッケージマネージャと、REPLやデバッグエクスペリエンスに使用するlldbの開発が完了しておらず、とりあえずはなにも考えずにMacでの開発をおすすめします。
どんどん情報は更新されているので、気になる方は「Swift Windows」で検索してみてください。

Xcodeの設定を知ろう

image.png
Xcodeを開くとこのような画面が出てきます。それぞれの説明をすると、

項目 説明
Create a new Xcode project 新規プロジェクト作成
Clone an existing project 既存プロジェクトのクローン
Open a project or file 既存プロジェクトのオープン

...そのまんまですね。
ちなみに下のチェックボックス「Show this window when Xcode launches」のチェックを外すと次回からこのメニュー画面が表示されなくなるので、ひとまず触らなくてOkです。

今回は新規作成なのでもちろん「Create a new Xcode project」(赤枠)を選択します。
スクリーンショット 2021-03-08 14.36.56.png
するとこちらの画面になるはずなので、iOSタブの「App」を選択します。
他にもたくさんアイコンが並んでいると思いますが、これらは最初に組まれる雛形が違うだけでいくらでもカスタマイズができるので、あまり難しく考えずに自身が作りたいものに最も近いものを選べばいいと思います。
今回はひとまず「App」で進めていきます。
スクリーンショット 2021-03-08 14.41.46.png
すると次はこちらの画面になるので、「Product Name」に今回のプロダクト名を入力します。今回はとりあえず「Sample」としておきます。
以下、他の項目については以下のような感じです。

項目 説明
Product Name プロダクト名、アプリ名
Team プロダクトを作成する団体名(もしくは個人名)
Organization Identifier プロダクトを作成する団体のID(もしくは個人)
Bundle Identifier 今回のID(Organization Identifier + . + Product Name)
Interface 実装方法の設定(SwiftUI, StoryBoardから選択)
Life Cycle ライフサイクルの選択
Languages 使用するプログラミング言語(Swift、 Objective-Cから選択)

上半分に関しては自身で設定すれば良いのですが、下部に関しては少し知識が必要かもしれません。

InterFaceについて

SwiftのインターフェイスはSwiftUI, StoryBoardの2つとなっていますが、これらの違いはコードでUIを実装するか、GUIでUIを実装するか。といったところです。
以前まではStoryBoard, Xib, UIKitの3つが存在していたのですが、iOS13にてSwiftUIが追加されました。
どれを選択するかは一概には言えず、アプリの特徴や開発者の慣れなどで考えるのがいいと思います。
ひとまず今回はサンプルを動かすだけなので、何も考えずにSwiftUIを選択します。

Life Cycleについて

Life Cycle(ライフサイクル)ってなんだ...わかります。その気持ち。知ってた方は飛ばしてちゃってください。
私もSwift学習を開始してから知ったので、知らなくても全く問題ないです。
ライフサイクルはアプリ内の状態遷移を表す言葉です。...どゆこと。
実際にiPhoneでアプリを使用するときを想像してみてください。
1.アイコンをタップする
2.画面が起動画面に切り替わる
3.画面がアプリ画面に切り替わる
4.画面のみ閉じるとアプリがバックグラウンドへ
5.再度起動すると前回使用画面から起動
6.タスクキルでアプリ終了
こういった一連の流れでアプリ側が保持している状態をライフサイクルといいます。
だいぶ説明を省いているので詳しく知りたい方は「アプリ ライフサイクル」で検索してみてください。

今回は「SwiftUI App」でOKです。

Languageについて

SwiftとObjectiv-Cからの選択です。(上記InterFaceでSwiftUIを選択している場合Swiftのみ選択可能)
こちらは使用言語についてです。
SwiftとObjective-Cの選択に関しては、一概には言えませんがSwiftの方が新しいことや、Swiftの方がAppleがSwiftを推奨していることからざっくりとSwiftの方が良いとされています。
ちゃんとした理由を知りたい方はご自身で調べることをおすすめします。

下部の選択項目について

上記の項目の下に写真のような選択項目があるのですが、これらに関しては一応軽い説明を載せますが、今回は全て選択しなくて大丈夫です。

Use Core Data / Host in CloudKit

「Use Core Data」には名前の通りCore Dataというものを使用するかどうかの選択です。
Core DataというのはざっくりXcode上からデータベース構造の設定を行えたり、使用するデータの保存、削除、更新を行うプログラムを書くための仕組みのことです。
こちらに関しても詳細はご自身で調べてください。(ちゃんと説明するとそれだけで1記事分になってしまうので...)
「Host in CloudKit」に関してですが、これはCloudKitを理解することから始まります。
簡単に言えばiCloudにデータを保存できるサービスです。AppleサポートのCloudKitのページを見てみると、CloudKitを使用するとキー値データ、構造化データ、および各種アセットをiCloudに保存できるようです。そして公開データベースと非公開データベースの両方に対応しているので、プライベートなデータ、パブリックなデータ両方扱えることになります。例えばAppleIDを使用してプライベートなデータを管理することも可能です。これはiOS標準アプリの「メモ」などにも使用されており、iPhoneでメモに何か保存すると、AppleIDで同期されているmacなどのメモにも表示されるようになります。これはCloudKitによって管理されていたんです。

Include Tests

続いて「Include Tests」についてです。これに関しては単純で、チェックを入れることでテスト用のターゲットとテスト用のコードが生成されます。なのでとりあえずはチェックを入れないで大丈夫です。

これですべての設定を終えたので、右下のNextボタンを押して次に進みます。

まとめ

ひとまず今回はここまでです。
xCodeは日本語版が無いということで設定内容を理解することだけで一苦労ですね...。
次回からはサンプルコードを使用してエディター内の項目解説に移るので、更新しましたらぜひ「Swift初心者がアプリを作れるようになるまで②」もご覧ください。最後までお読みいただきありがとうございました!

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

xcodeでデバッグするためのプロビジョニングプロファイル作成

概要

<xcode経由でデバッグ>
方法1:
Automatically manage signing
をチェックすると
AdHocのプロビジョニングプロファイルはなくても、Xcodeで
自動で選択して、なければ自動生成できる。
https://qiita.com/rotors123/items/a015ff0c3c3ed3cf5c47

方法2:
Automatically manage signingの
チェックを入れずに以下を実施する
証明書要求ファイルを作る(以前作ったものがあればそれを使える)
Development証明書を作る
Developmentプロビジョニングを作る。
iPhone端末に上記をインストールする
ビルドターゲットにそのプロビジョニングプロファイルを指定してビルド

<ipaでインストール>
証明書要求ファイルを作る(以前作ったものがあればそれを使える)
Distribution証明書を作る(Store用とAdHocは同じものを使える)
DistributionのAdhocプロビジョニングを作る
ビルドターゲットにそのプロビジョニングプロファイルを指定
アーカイブでipaを出力
※デバイス追加の都度、UUID登録、Provisioning Profile更新、リビルド等の作業が必要

<store公開>
証明書要求ファイルを作る(以前作ったものがあればそれを使える)
Distribution証明書を作る(Store用とAdHocは同じものを使える)
DistributionのStoreプロビジョニングを作る
ビルドターゲットにそのプロビジョニングプロファイルを指定
アーカイブでipaを出力

<補足>
参考手順
https://hirokuma.blog/?p=2783

プロビジョニングプロファイルの有効期限(1年)が過ぎた場合は更新が必要になる
https://qiita.com/Labi/items/d93624e4f6ec7d073e76

build schme(起動時に端末と一緒に選択する実行環境 = 以下をまとめたもの)
・build target(ビルド情報:プロビジョニングプロファイルとかを設定)
・build configuration(コード上でdebug/releaseを分けたいときに利用)
->build targetを追加すると、自動でbuild schmeも追加される
https://qiita.com/kazy_dev/items/feb68f162ec3d91005d3

xcodeでデバッグするためのプロビジョニングプロファイル作成

以下を参考に、証明書要求ファイルを作っておく
https://i-app-tec.com/ios/apply-application.html

↓以下の「+」を押下

image.png

↓ iOS App Developmentを選択
※iOS Distribution (App Store and Ad Hoc)のApp Storeはストア公開用、Ad Hocはipaでのインストール用

image.png

↓証明書要求ファイルを選択

image.png

↓ダウンロード(ios_development.cer)
↓ダウンロードできたらダブルクリックしてキーチェーンに登録

image.png

↓+をクリック

image.png

image.png

↓Apple IDを選択

image.png

image.png

↓上記Create Devicesで、一旦端末登録画面に飛ばされる
XcodeのDevvicesのSerial Number: の下の IdentifierがUUID

image.png

image.png

image.png

image.png

↓わかりやすい名前をつけてダウンロード

image.png

image.png

↓+を選んで、さっきのプロファイルを選んでinstall

image.png

最後に、signingのprovisioning Profileに先程のプロビジョニングプロファイルを設定すると、実機デバッグが可能になる
※ビルドスキームを追加すると、実行時のスキームを切り替えるとプロビジョニングプロファイルを切り替えれるので便利。以下手順参考
https://qiita.com/kazy_dev/items/feb68f162ec3d91005d3

補足1 ipaでデバッグするためのプロビジョニングプロファイル作成

image.png

Generate a Provisioning Profileページ
->AdhocはDistribution用の証明書を使うため、ストア公開用の証明書を使えるので、それを選択

作成されたらプロビジョニングプロファイルをダウンロードし、ビルドターゲットに指定
端末はAny iOS Deviceを選択し、上記のビルドターゲットを指定してProduct -> Archive
Validate Appはせずに
Distribute App->Ad Hoc->プロビジョニングプロファイルはAd Hoc用を選択。
Distribution CertificateはDistribution用の証明書を使うため、ストア公開用の証明書を使えるので、それを選択
ipaがExportされるので、Apple Configurator 2などでインストールする

参考
https://makotton.com/2015/06/23/1063

補足2 Testflightの内部テスト

Appstore Connectで
ユーザーとアクセスを選択し、画面に従ってユーザーを登録
image.png

ユーザーに招待メールが届くので、それに同意してもらう

マイApp -> Testflight
スクリーンショット 2021-03-22 0.18.15.png

App Store Connectをユーザ選択 -> ユーザーを登録すると、招待メールが届く
image.png

ユーザーは招待メールにアクセスし、Testflightアプリをインストールすると、Testflight経由でテストができる
(この方法は「内部テスト」という仕組みで、審査不要。「外部テスト」というのはテスト用の審査が必要)
https://qiita.com/haseken_dev/items/8c094b44c7e67836b74d
https://qiita.com/john-rocky/items/9e3538bfec0300cdc7ea

補足3 Store公開用のプロビジョニングプロファイル作成参考サイト

https://www.small.co.jp/application-develop/ios-%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92-adhoc-%E3%81%A7%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B/
Ad Hocを後悔するのはこういう方法もあるみたい

公開用のwebサイトを用意する
ipaファイル・plistファイルを同じディレクトリに配置する
webページにplistファイルを開くリンクを設ける(これをiosのブラウザでクリックするとアプリのインストールが実行される)
※plistファイルを編集することでipaファイルのパスを任意に指定すること等も可能

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

SwiftUIでフルスクリーンのモーダルを表示する(iOS13)

SwiftUI(iOS13)でフルスクリーンモーダルを表示する

久しぶりの記事になります。おはこんばんにちは和尚です!
今日は一昨年Appleから発表されて話題となっているSwiftUIについての記事を書いていきたいと思います:v:

SwiftUI 2はiOS13では使用できない件

現在(2021.3月時点)ではSwiftUIはver.2までリリースされておりますが、残念なことにSwiftUI 2でリリースされた機能はiOS13で使用することができません。SwiftUI 1では一般的なアプリに必要な機能が全然揃っておらず、2以上にライブラリやUIKitに頼ることになります。

必須級ライブラリ
SwiftUIX
↑ SwiftUIにまだ実装されていないものを補ってくれます。説明書がないものがチラホラあるのが残念...

さて、本題であるフルスクリーンのモーダルですが、公式からは「.fullScreenCover」というものが用意されていますがこちらはSwiftUI 2から登場したものとなっており残念ながらiOS13では使用することができません。
ので、今回iOS13でも使えるフルスクリーンのモーダルを作っていきましょう!

※ちなみに筆者、初めてのiOSアプリ実装がSwiftUIとなっておりUIKitも現在絶賛勉強中ですのでお手柔らかに(笑)

フルスクリーンのモーダル実装

1. UIApplicationの拡張

UIApplicationを拡張して一番上のコントローラーを取得するメソッドと、フルスクリーンのモーダルを閉じるメソッドを作成していきます。

UIApplication+Extension.swift
extension UIApplication {
    /// 一番上にあるコントローラーを取得する
    public func getTopViewController() -> UIViewController? {
        guard let window = UIApplication.shared
                .connectedScenes
                .filter({$0.activationState == .foregroundActive})
                .map({$0 as? UIWindowScene})
                .compactMap({$0})
                .first?.windows.first else {
            return nil
        }

        window.makeKeyAndVisible()

        guard let rootViewController = window.rootViewController else {
            return nil
        }

        var topController = rootViewController
        while let newTopController = topController.presentedViewController {
            topController = newTopController
        }

        return topController
  }

    /// フルスクリーンのモーダルを閉じる
    public func closeModalView() {
        UIApplication.shared.getTopViewController()?.dismiss(animated: true, completion: nil)
    }
}

2. Viewの拡張

次はSwiftUIのViewでSheetViewやFullCreenCoverのようにフルスクリーンのモーダルが使用できるようにViewを拡張していきます。

View+Extension.swift
extension View {
    public func fullScreenView<Content>(
        isPresented: Binding<Bool>,
        @ViewBuilder content: @escaping () -> Content
    ) -> some View where Content: View {
        if isPresented.wrappedValue {
            let window = UIApplication.shared.windows.last
            window?.isHidden = true

            let view = content()
            let viewController = UIHostingController(rootView: view)
            viewController.modalPresentationStyle = .fullScreen

            DispatchQueue.main.async {
                guard let tvc = UIApplication.shared.getTopViewController() else {
                    return
                }

                tvc.present(viewController, animated: true, completion: nil)
                isPresented.wrappedValue = false
            }
        }

        return self
    }
}

実際に使ってみよう!

実際にViewで使用してみましょう!今回適当なサンプルViewを用意してみました!
こちら参考にみなさん是非使ってみてください:thumbsup:

ContentView.swift
struct ContentView: View {
    @State private var showModal: Bool = false

    var body: some View {
        VStack {
            Spacer()
            Button(action: {
                showModal.toggle()
            }, label: {
                Text("フルスクリーンのモーダルを表示する")
            })
            Spacer()
        }
        .fullScreenView(isPresented: $showModal) {
            ModalView()
        }
    }
}

struct ModalView: View {
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)
            Button(action: {
                UIApplication.shared.closeModalView()
            }, label: {
                Text("閉じる")
            })
        }
    }
}

ezgif-1-6f84b46cee07.gif

最後に

iOS13でSwiftUIとCombineを使用する際は、特にiOS13.0からiOS13.2までは気をつけましょう。バグがかなり多いです...
しかも、たちが悪いことにシュミレーションでは再現せず実機のみで再現するバグもいくつかあります。

実機がない場合は、SwiftUIをiOS13で使用することをおすすめしません:frowning2:
それでもiOS13を含めたい場合は最低でもiOS13.3以上にしましょう。

Let's Enjoy SwiftUI!!

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

【Swift】IntにSetAlgebraを実装して集合のように扱えるようにする

Intのビットを使って集合演算をするとき、ビット演算をSetAlgebraで覆ってあげたほうが使いやすいかもと思って書いてみた。

関数を一つでも削るとコンパイルが通らないような極小の実装になっている。

let a: Int = [0, 2, 3]
let b: Int = [0, 2, 4]
assert(a.union(b) == 1 + 4 + 8 + 16)
assert(a.union(b) == [0, 2, 3, 4])
assert(a.intersection(b) == [0, 2])
assert(a.symmetricDifference(b) == [3, 4])
assert(a.isSubset(of: [0, 1, 2, 3]))

extension Int: SetAlgebra {
    public typealias Element = Int

    public func contains(_ member: Element) -> Bool {
        self >> member & 1 == 1
    }

    public func intersection(_ other: Self) -> Self {
        self & other
    }

    public func union(_ other: Self) -> Self {
        self | other
    }

    public func symmetricDifference(_ other: Self) -> Self {
        self ^ other
    }

    public mutating func formIntersection(_ other: Self) {
        self &= other
    }

    public mutating func formUnion(_ other: Self) {
        self |= other
    }

    public mutating func formSymmetricDifference(_ other: Self) {
        self ^= other
    }

    public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) {
        if contains(newMember) {
            return (false, newMember)
        } else {
            self |= 1 << newMember
            return (true, newMember)
        }
    }

    public mutating func remove(_ member: Element) -> Element? {
        if contains(member) {
            self &= ~(1 << member)
            return member
        } else {
            return nil
        }
    }

    public mutating func update(with newMember: Element) -> Element? {
        if contains(newMember) {
            return newMember
        } else {
            self |= 1 << newMember
            return nil
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む