20210121のiOSに関する記事は6件です。

iPhoneの壁紙に飽きたので、毎日ちがう壁紙を設定してほしい

iPhoneを使い始めてしばらく経ちましたが、壁紙を見飽きてしまいました。
毎日ちがう壁紙を設定したい!しかも自動で変更してほしい!もっと言えば、探すのも面倒なので、いい感じの壁紙を見繕って来てほしい!

ということで作りました。
比較的新しめのiPhone標準アプリ、ショートカットで夢を叶えることができました!

いい感じの壁紙

今回は、Bingの壁紙を使うことにしました。毎日いい感じに変わり、PC/スマホともに壁紙をダウンロードすることもできます。

作成したショートカットを共有します

作成したショートカットの共有リンクを掲載しておきます。ご自由にお使いください。
https://www.icloud.com/shortcuts/1372925c0aa14d5399ae02c907d1be26

今回はBingを使いましたが、いい感じの画像をゲットできるAPIなどがあれば、お好みで上記ショートカットをカスタマイズしても良いと思います。
なお、壁紙の変更はiOS14.3以上のみ利用可能らしいです。(未確認)

自動的に壁紙を変更する

上記ショートカットだけでは、壁紙は自動で変更されません。

ショートカットアプリのオートメーション個人用オートメーションを作成時刻からお好きな時刻を設定→アクションを追加から、ショートカットを実行を設定します。(ショートカットを実行はスクリプティングの中にあります)

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

【Swift】Combine + APIKit で複数リクエストを直列で実行するサンプル (ついでに並列も)

title.png

Combine.frameworkを体験する取っ掛かりとして
APIKitのRequestにPublisherを生やして
複数の非同期通信を直列・並列で叩くサンプルを実装してみました。

まだまだCombineニュービーですのでオススメの方法や
間違い等有りましたらご指摘頂けると幸いです??‍♂️

環境

  • Xcode 12.1
  • APIKit 5.1.0
  • SwiftUI

サンプルコード

https://github.com/SatoshiN303/RequestsWithCombine

プロジェクト構成

APIKitにPublisherを生やす実装方法については
Developers.IO さんの以下の記事が
非常にわかりやすかったのでほぼコピさせていただきました。
[iOS 13] SwiftUI + Combine + APIKitでインクリメンタルサーチ

今回のサンプルコードでは複数の通信を叩くために
GitHub REST APIを使う形に変更しています。

スクリーンショット 2021-01-21 16.00.27.png

処理の流れ

主な登場人物は上記赤枠の3つです。
処理の流れは以下になります。

  1. ContentViewUISearchBarの入力を受け取る
  2. SampleModelでリクエストを作成して直列の通信を開始する
  3. 最初に/search/repositoriesを叩く
  4. 上記のレスポンスを利用して/repos/{owner}/{repositry}を叩く
  5. 通知された結果を表示する

直列で叩く(Publisher)

最初に/search/repositories を叩き、レスポンスを利用して
/repos/{owner}/{repositry} を叩いてみます。
正直最初のリクエストで欲しいデータは取得できますが学びということで。

        // `/search/repositories` のリクエスト作成
        let searchRepositories = SearchRepositoriesRequest(query: searchText).publisher.eraseToAnyPublisher()
        self.requestCancellable = searchRepositories
            // レスポンスはSearchRepositoriesResponse型なのでitems.firstをとりあえず取得
            .compactMap { $0.items.first }
            // 上記レスポンスを利用して GetRepositoyRequest を作成
            .flatMap { (repository) -> AnyPublisher<GetRepositoyRequest.Response, Error> in
                /* 処理を繋げるとコンパイラが型がネストして解釈して複雑な型になるため
                 * eraseToAnyPublisher()で型消去した
                 * AnyPublisher<GetRepositoyRequest.Response, Error> を返す
                 */ 
                GetRepositoyRequest(owner: repository.owner.login, repositry: repository.name).publisher.eraseToAnyPublisher()
            }
            .receive(on: DispatchQueue.main)
            .sink { (completion) in
                switch completion {
                case .finished:
                    debugPrint("request finished")
                case let .failure(error):
                    debugPrint("request failed : \(error)")
                }
            } receiveValue: { [weak self] repository in
                // `/repos/{owner}/{repositry}` のレスポンスをContentViewに渡す
                self?.items = [repository]
            }

無事直列で叩けてる模様です。
スクリーンショット 2021-01-21 17.18.37.png

補足リンク eraseToAnyPublisher()での型消去について

https://developer.apple.com/documentation/combine/publisher/3241548-erasetoanypublisher

Futureを使って直列で叩く

FutureはSubscribeされなくても
インスタンスが生成されたタイミングで即時実行されるので
とりあえずクロージャーで返すメソッドを作って処理を繋げてみました。
こちらも本来であれば再利用のためにAPIKitに生やすのが望ましいでしょうか。

        // MARK: - Futureを使って直列で叩く

        self.requestCancellable = fetchRepos(query: searchText)
            .compactMap { $0.items.first }
            .flatMap { [unowned self] repository in
                self.fetchRepository(owner: repository.owner.login, repo: repository.name)
            }.sink { (completion) in
                switch completion {
                case .finished:
                    debugPrint("request finished")
                case let .failure(error):
                    debugPrint("request failed : \(error)")
                }
            } receiveValue: { [weak self] repository in
                self?.items = [repository]
            }


    // MARK: - 各リクエストの生成処理

    // ※Futureは即時実行される => インスタンスが生成されたタイミングでSubscribeをしなくても処理が走る
    private func fetchRepos(query: String) -> Future<SearchRepositoriesRequest.Response, Error> {
        return Future<SearchRepositoriesRequest.Response, Error> { promise in
            Session.send(SearchRepositoriesRequest(query: query), callbackQueue: .main) { (result) in
                switch result {
                case .success(let res):
                    // 処理成功
                    promise(.success(res))
                case .failure(let error):
                    // 処理失敗
                    promise(.failure(error))
                }
            }
        }
    }

    // ※Futureは即時実行される => インスタンスが生成されたタイミングでSubscribeをしなくても処理が走る
    private func fetchRepository(owner: String, repo: String) -> Future<GetRepositoyRequest.Response, Error> {
        return Future<GetRepositoyRequest.Response, Error> { promise in
            Session.send(GetRepositoyRequest(owner: owner, repositry: repo), callbackQueue: .main) { (result) in
                switch result {
                case .success(let res):
                    // 処理成功
                    promise(.success(res))
                case .failure(let error):
                    // 処理失敗
                    promise(.failure(error))
                }
            }
        }
    }

こちらも無事に直列で叩けてる模様です。
スクリーンショット 2021-01-21 17.19.50.png

ついでに並列でも叩いてみる

Publishers.Zipに渡せば並列で叩けるので
/search/repositories/search/usersを同じクエリで叩いてみます。

        let searchRepositories = SearchRepositoriesRequest(query: searchText).publisher.eraseToAnyPublisher()
        let searchUsers = SearchUsersRequest(query: searchText).publisher.eraseToAnyPublisher()

        self.requestCancellable = Publishers.Zip(searchRepositories, searchUsers)
            .receive(on: DispatchQueue.main)
            .sink { _ in } receiveValue: { (repos, users) in
                print(repos)
                print(users)
            }

無事叩けてました。
スクリーンショット 2021-01-21 17.21.01.png

ついでに補足比較: RxSwiftで直列で叩く場合

RxSwiftの場合はSessionにObservableを生やしてflatMapで繋ぐ感じでしょうか。

extension APIKit.Session {
    func rx_sendRequest<T: Request>(request: T) -> Observable<T.Response> {
        return Observable.create { observer in
            let task = self.send(request) { result in
                switch result {
                case let .success(res):
                    observer.on(.next(res))
                    observer.on(.completed)
                case let .failure(err):
                    observer.onError(err)
                }
            }
            return Disposables.create {
                task?.cancel()
            }
        }
    }
}

   // Observableで包んで返す

    func searchRepositories(request: SearchRepositoriesRequest) -> Observable<SearchRepositoriesRequest.Response> {
        return Session.rx_sendRequest(request: request)
    }

    func getRepository(request: GetRepositoryRequest) -> Observable<GetRepositoryRequest.Response> {
        return Session.rx_sendRequest(request: request)
    }

    // もろもろ省略

    let observable = searchRepositories(request: searchRepositoriesRequest)
            .flatMap { [unowned self] repos -> Observable<GetRepositoryRequest.Response> in
                let item = repos.items.first //とりあえず成功してアンラップできてるものとみなす
                let getRepositoryRequest = getRepositoryRequest(owner: item.owner.login, repo: item.name)
                return self.getRepository(request: getRepositoryRequest)
            }.もろもろ続く

参考にさせて頂きました。ありがとうございます。

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

結局ScrollViewのConstraintってどうつければいいの?

  1. UIScrollViewの上下左右をUIViewControllerのViewに合わせる
  2. ContentViewの上下左右をContent Layout Guideに合わせる
  3. ContentViewの横幅をFrame Layout Guideに合わせる(Equal Widths)
  4. ContentViewにオブジェクトを追加して高さを確定させる

名前の通り、Content Layout GuideはUIScrollViewの中身、Frame Layout GuideはUIScrollViewのフレーム領域を示します。
https://qiita.com/owen/items/2fab4a4482834b95e349

つまづきやすかったこと

ContentView は自分で UIView を追加して作る

UIViewで囲むのがセオリーとは思うものの、ちょっとAppleさん不親切‥

Content Layout Guide の制約に注意

ContentView に Content Layout Guide の上下左右の制約をかけるとき、ちゃんと画面いっぱいに伸ばしてCtrlで制約を引っ張ってもなぜか+315や+812とズレがつく。見た目では気づきにくいが、これを0に修正しないとスクロールしないので注意。
スクリーンショット 2020-12-31 18.48.43.png
スクリーンショット 2021-01-21 10.35.31.png

高さを確定させよ

中のコンテンツの一番下の要素にContentView.bottomとのconstraintを付けないと高さ確定しないので怒られる。

よくある間違い?

ContentViewのbottomをFrame Layout Guideに合わせてしまう

正しい設定では中の要素が少ない時に下側がスカスカに空いてしまう場合がある。それを埋めようとContentViewのBottomをFrameに合わせると、FrameはiPhoneの上下左右の見えてる部分らしく、本来もっと下に伸びるべきContentViewのBottomがiPhoneの底で止まってスクロールしなくなる(伝われ)。
スクリーンショット 2021-01-21 10.35.21.png
中身が少なく下が空いてしまうときは、ContentViewとScrollViewの背景色をあわせて空いてないように見せるしかないのかしら?

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

[SwiftUI] SceneDelegate を消して ApplicationDelegate だけにする

これはなに

iOS 13 環境で ReplayKit & UIScene の相性が悪いため、泣く泣く UISceneDelegate を消して UIApplicationDelegate だけにしたときのメモです。

ReplayKit & UIScene: Assertion when calling RPSystemBroadcastPickerView · Issue #438 · twilio/video-quickstart-ios

If you must use RPSystemBroadcastPickerView then you should consider not using UIScene, and going back to UIApplicationDelegate.

やったこと

SceneDelegate をバッサリ削除

Info.plist から UIApplicationSceneManifest をバッサリ削除します。

image.png

その後、SceneDelegate.swift も不要になるので削除します。(SceneDelegate でやっていたことがある場合は AppDelegate に移行する必要がある)

AppDelegate から UIHostingController経由で SwiftUI の View を開く

AppDelegate.swift を次のように書き換えます。

import UIKit
import SwiftUI

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow(frame: UIScreen.main.bounds)
        window!.rootViewController = UIHostingController(rootView: ContentView)
        window!.makeKeyAndVisible()

        return true
    }
}

このとき、func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
のような UISceneSession Lifecycle が残っているとうまくアプリが動かないので注意です。

これで一応アプリが起動するようになりますが、動作を保証するものではありません。あくまで自己責任で!

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

ダークモードにまつわる誤解を解いておく

Githubがダークモードに対応したのが昨年12月。調べて見るところ我々開発者には「目に優しい」と認識されている一方、一般層には単に「格好いいモード」と思われていたりなようです。更にそれだけでもないという記事を見つけました。

概要

以下、流行ってるな~と思ったので、まとめました。「ダークモード」のメリット、デメリット。本当のところ。GitHubをダークモードで使う
image.png
ダークモードに対応したデザイン は2020年始めからトレンドとして指摘されていました。
Redmineでダークモードを使う なんてこともできるようだ。

ダークモードのメリット (とされているもの)

  • 画面全体の輝度が下がるので目の負担が減る、疲れにくい。
  • 特に暗い環境ではコントラストをあげられるので、見やすい。
  • 有機ELパネルのディスプレイスマホなら消費電力が下がって電池が長持ちする。
  • 「格好いい」感じがするとよく言われる。

image.png
引用: https://blog.weekdone.com/why-you-should-switch-on-dark-mode/

デメリット (とされているもの)

  • スマホでは、未対応のアプリと画面の明るさの差が大きくなる。
  • 細かく配慮されていない画面ではアイコンやボタンが見にくいものがある。
  • 夜中にスマホの光を懐中電灯代わりにできない(?)。

実際は

「ダークモード」は、本当に“目に優しい”のか? 5つの観点から科学的に検証した結果 が詳しい。以下サマリ。

目の疲れを軽減する?

「確かな証拠を、わたしはまだ見たことがありません」。最善の解決策は、コンピューターの利用を控えることである。

集中力が高まる?

疑問あり。実験参加者に視力検査と校正作業を行ってもらったところ、白い背景に黒い文字のほうが読む速度が速く、見つけた誤字脱字の数も多かった。またTwitterは、ダークモードを有効にしているユーザーのほうが、アプリに費やす時間が長くなることを発見した。だが、これは集中力が増すためではなく、ベッドに入ったまま画面をスクロールするには、ダークモードのほうが不快感が少ないからかもしれない。

「ダークモードは集中力を高め、わたしたちの気持ちを落ち着かせると謳っているアプリはたくさんある。しかし、このモードを搭載する本当の動機は、単にユーザーの利用時間を長くできるからなのかもしれない。」

就寝前はダークモード?

No. 最も効果の高い方法は、眠りにつく1~2時間前には画面を見るのを完全にやめることである。

理想形

文献は記事最後に記載してあります。ダークモードがどこでも何でも良しということはなく、格好いいとか、好みの問題でもないようです。スマホをダークモードにする欠点|逆に目疲れの原因に?、と、子どもには逆に有害とすらする記事も。

例えば、明るい部屋や直射日光の当たる場所では、暗い背景に置かれたテキストが読みづらくなることもある。画面を専ら明るい昼間の屋外で使用したり、屋内・屋外を頻繁に行き来するユースケースでは、例外的な配慮が必要といえる。例えば日没、日の出時刻、自動検知による自動切り替え機能が必要になる。

「バッテリー寿命を延ばす?」という説については、True。
有機ELディスプレイを採用したAndroidのスマートフォンをダークモードにして「Google マップ」のスクリーンショットを表示したところ、消費電力は通常モードより63パーセント少なかったという。(「ダークモード」は、本当に“目に優しい”のか? 5つの観点から科学的に検証した結果 )

目には、休息が一番。このような小話ばかり読んでいないで休むときにはしっかり休みましょう。

ダークモードを搭載する例

OS

Windows 10
Mac OS
Android
IOS

ブラウザ

Google Chrome
Mozilla Firefox

チャットアプリ

Facebook Messenger
Whatsapp
Skype
Slack

SNS

Twitter
facebook

ほか

VSCode
IntelliJ
Eclipse
Github
Todoist
Trello

参考

ダークモードのメリット、デメリットはなんですか?
2020年流行するであろうwebデザインのトレンド
The pros and cons of Dark Mode: Here's when to use it and why
The Benefits of Dark Mode: Why should you turn off the lights?
「ダークモード」は、本当に“目に優しい”のか? 5つの観点から科学的に検証した結果
スマホをダークモードにする欠点|逆に目疲れの原因に?

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

XcodegenでExcluded Architectureにarm64を設定する

Xcodegenを使用したプロジェクトで、arm64アーキテクチャでビルドしようとしたら以下のようなエラーになった。

The linked framework 'Pods_[アプリ名].framework' is missing one or more architectures required by this target: arm64.

調べてみたところ、Build Settings > Architectures > Excluded Architectureで、Any iOS Simulator SDKを選び、arm64を追加すれば直るとのこと。しかし、Xcodegenを使ってプロジェクトファイルを生成していたので、project.ymlでの書き方がわからず少しハマった。

以下のように、ターゲットのsettingsに対して、EXCLUDED_ARCHS[sdk=iphonesimulator*]: arm64としたら、うまく生成できたのでそのメモ。

project.yml
targets:
  YourApp:
    type: application
    platform: iOS
    sources:
      - path: YourApp
    settings:
      base:
        EXCLUDED_ARCHS[sdk=iphonesimulator*]: arm64

参考

https://stackoverflow.com/questions/63607158/xcode-12-building-for-ios-simulator-but-linking-in-object-file-built-for-ios
https://medium.com/@khushwanttanwar/xcode-12-compilation-errors-while-running-with-ios-14-simulators-5731c91326e9

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