20190820のSwiftに関する記事は5件です。

iOS13(beta)でASWebAuthenticationSessionの変更点

注:本記事は Xcode 11 beta 6 (2019/08/20)時点での変更内容での話になります

iOS13でログイン画面が開かない

開発中のアプリでASWebAuthenticationSessionを利用してOAuthログインする機能があり、iOS13(beta)で動作確認をしたところログインボタンを押してログイン画面を出そうとしても出てくれない状態でした。

ログを見てみると以下のエラーの出力がありました。

Cannot start ASWebAuthenticationSession without providing presentation context. Set presentationContextProvider before calling -start.

どうやらiOS13からはstart()を呼び出す前にpresentationContextProviderをセットする必要があるようです。

ASWebAuthenticationPresentationContextProviding

presentationContextProviderASWebAuthenticationPresentationContextProviding(document)というprotocolに適合したNSObjectのサブクラスのインスタンスのようです。

定義は以下のようになっています。

/** @abstract Provides context to target where in an application's UI the authorization view should be shown.
 */
@available(iOS 13.0, *)
public protocol ASWebAuthenticationPresentationContextProviding : NSObjectProtocol {


    /** @abstract Return the ASPresentationAnchor in the closest proximity to where a user interacted with your app to trigger
     authentication. If starting an ASWebAuthenticationSession on first launch, use the application's main window.
     @param session The session requesting a presentation anchor.
     @result The ASPresentationAnchor most closely associated with the UI used to trigger authentication.
     */
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
}

ASPresentationAnchor

なるほど、そのprotocolのインスタンスを用意すればいいことはわかったけど、ASPresentationAnchorってなんだ?と思ったらUIWindow(iOS, Mac Catalyst, tvOS) or NSWindow(macOS)のtypealiasでした。

直してみる

シンプルに書くと多分こんな感じに実装すれば良さそう

import AuthenticationServices
import UIKit

class ViewController: UIViewController {

    private var authenticationSession: ASWebAuthenticationSession?

    // ログインボタンを押した後に呼ばれる
    func loginStart() {
        let authenticationSession = ASWebAuthenticationSession(
            url: URL(string: "https://...")!,
            callbackURLScheme: "<url-scheme>",
            completionHandler: loginCompletionHandler(url:error:))
        if #available(iOS 13.0, *) {
            // 
            authenticationSession.presentationContextProvider = self
        }
        authenticationSession.start()

        // authenticationSessionが解放されないようにプロパティに入れておく
        self.authenticationSession = authenticationSession
    }

    // ログインの結果のハンドリング
    func loginCompletionHandler(url: URL?, error: Error?) {
        authenticationSession = nil
        ...
    }

    ...
}

@available(iOS 13.0, *)
extension ViewController: ASWebAuthenticationPresentationContextProviding {

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
            return view.window!
    }
}

ちなみに今開発中のアプリではiOS11も対応しており、ASWebAuthenticationSessionSFAuthenticationSessionをラップしたクラスを定義していたため、ASWebAuthenticationPresentationContextProvidingを適合させたprivateなクラスを用意して実装しました。
このように実装する場合、presentationContextProviderは弱参照なので解放されてしまいます。そのためASWebAuthenticationPresentationContextProvidingを適合したオブジェクトのインスタンスもプロパティとして保持しないといけないことに注意してください。

class AuthenticationSession {

    ...
    private var authenticationSession: Any?
    private var presentationContextProvider: Any?

    func loginStart(from viewController: UIViewController) {
        if #available(iOS 12.0, *) {
            let authenticationSession = ASWebAuthenticationSession(
                url: loginURL,
                callbackURLScheme: "<url-scheme>",
                completionHandler: loginCompletionHandler(url:error:))
            if #available(iOS 13.0, *) {
                let presentationContextProvider = AuthPresentationContextProver(viewController: viewController)
                authenticationSession.presentationContextProvider = presentationContextProvider
                self.presentationContextProvider = presentationContextProvider
            }
            authenticationSession.start()
            self.authenticationSession = authenticationSession
        } else { // iOS 11.x
            let authenticationSession = SFAuthenticationSession(
                url: loginURL,
                callbackURLScheme: "<url-scheme>",
                completionHandler: loginCompletionHandler(url:error:))
            authenticationSession.start()
            self.authenticationSession = authenticationSession
        }
    }

    func loginCompletionHandler(url: URL?, error: Error?) {
        authenticationSession = nil
        presentationContextProvider = nil
        ...
    }

    ...

    @available(iOS 13.0, *)
    private class AuthPresentationContextProver: NSObject, ASWebAuthenticationPresentationContextProviding {

        private weak var viewController: UIViewController!

        init(viewController: UIViewController) {
            self.viewController = viewController
            super.init()
        }

        func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
            return viewController?.view.window!
        }
    }
}

おわり

ASWebAuthenticationSessionを使っているアプリはiOS13が出るまでに対応しないとログインが出来なくなってしまう可能性があるので注意しましょう。

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

[30秒で理解]SwiftでDI(Dependency Injection)

最近、少しばかりKotlinでAndroid開発をしているときにSwift以上にKotlinの方がDIの文化が進んでそうだな(主にDaggerとか)と思ったんですが、意外とちゃんと時間をとってSwiftのDIについて調べていないなと思ったので時間をとって記事にした次第です。

Dependency Injectionとは

名前の格好良さとか複雑さに惑わされて欲しくはないので言いますが、

ただ外部から値を渡して上げて受け取った物(主にclass)は受け取ったことを前提に記述することできる

ただそれだけです。

実際にcodeに起こしたときはこんな感じです。

class Hoge {
    var name: String

    init(name: String) {
        self.name = name
    }
}
let hoge = Hoge(name: "hogehoge")

これが一番シンプルなDIと言えると思います。DIってなんだろうと思って検索した場合は、これは当たり前のことだし、これをDIっていうの思った人も少なくないと思います。ライブラリを使うとコンテナとかファクトリとかいろいろな単語が出てきて混乱するかもしれませんが、DIという手法をより有効的に使用するための要素でその手法本体ではありません。

それでもまだ信じられない人はこの記事をみてみることを推奨します

https://qiita.com/ostk0069/items/03c968bacf8e1372b7e7

この記事は以前私が書いた物ですが、UITableViewを使うときにUITableViewCelldequeue(呼び出す)するタイミングでパラメータを挿入しているだけです。上のサンプルコードでもnameを挿入しているのと同様です。

しかし、これだけではおわりません。いまの説明で完全に理解するにはDIのDの方、Dependencyへの理解が不十分です。ここでいうDependencyとはSingletonであると考えて良いです。

Singletonについて

Singletonとはその物(class, protocol)が実行時に一つのオブジェクトにのみ依存することを約束することです。ここでSwiftにおいてSingletonを約束するために必要なのがコンテナという概念です。これ以上深く話すには実際にDIのライブラリ等でDIのコンテナの解釈のもと、ライブラリが作られているかで少し変わってきます。思想の問題です。

Swiftで書かれているDIのライブラリ

コンテナを用いる場合

コンテナを用いない場合

その他(injectionが綺麗にかけるライブラリ)

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

iOS13でUISegmentedControlの見た目をカスタマイズする

※ 実装/動作確認環境: Xcode 11 beta5 + Simulator
※※ iOS13正式リリース前につき、スクリーンショットは未掲載(100%見た目についての話なのでなんとも物足りない感じですが…)。

背景

iOS13ではUISegmentedControlの見た目が変更される。
それに伴い、見た目(主に色)のカスタマイズをする場合、iOS12以前とは異なる方法で行う必要がある。

さしあたりの対応として行ったことをメモ。
(DarkMode対応なども鑑みるとカスタマイズなしで使うようにUIを設計し直すのが妥当とは思うが…)

iOS12以前

tintColorがビューやラベルの色に適用されるため、ここに任意の色を指定するだけでOKだった。

tintColor = UIColor.orange

iOS13

デフォルトの表示が無彩色となり、tintColorに依存しなくなった。

以下の方法でそれなりにカスタマイズできる。しかしそれぞれ一長一短あり。

selectedSegmentTintColor を使用する方法

tintColor ではなく selectedSegmentTintColor を指定することで、選択中のセグメントの背景色を設定できる。
また、文字色はデフォルトで黒色であるため、設定する背景色によっては文字色も変更したほうがベター。

func configure() {
  let color = UIColor.orange

  selectedSegmentTintColor = color
  setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], for: .selected)
  setTitleTextAttributes([NSAttributedString.Key.foregroundColor: color], for: .normal)
}
メリット

コード量も少なく、やっていることも明確である点。
基本的にはこの程度のカスタマイズで収めたい。

デメリット

この方法ではビューの背景にある薄いグレーはそのままなので、selectedSegmentTintColor の明度によってはコントラストが弱くなる恐れがある。
(View Hierarchyを確認したところ、SegmentControlのsubview内に半透明のグレー画像が設定されたUIImageViewがある模様)

コントラストに懸念があり、かつ色も変更できない場合は次の方法で薄いグレーの背景を除去するのがよさそう(ただし急場しのぎ。詳細は後述)。

setBackgroundImage() を使用する方法

func configure() {
  let color = UIColor.orange

  // 背景を単色画像に差し替え
  setBackgroundImage(UIColor.clear.toImage(), for: .normal, barMetrics: .default)
  setBackgroundImage(color.toImage(), for: .selected, barMetrics: .default)

  // ラベルのスタイルを設定(選択中のセグメントのラベルがBoldでなくなるので、合わせて設定)
  setTitleTextAttributes(
    [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 13.0),
     NSAttributedString.Key.foregroundColor: UIColor.white],
    for: .selected)
  setTitleTextAttributes([NSAttributedString.Key.foregroundColor: color], for: .normal)

 // グレー領域がなくなったことで境界がわかりづらくなるので、ボーダーを追加
 layer.borderColor = color.cgColor
 layer.borderWidth = 1.0
}

↑で使用している UIColor.toImage() は1x1の単色UIImageを生成する拡張メソッド。実装方法はこちらの記事等を参照。

メリット

背景のグレーがなくなるので、SegmentedControlの色をより自由に決められる点。

デメリット

ビューの構造も変わってしまうらしく、見た目はiOS12以前のSegmentedControlのようになってしまう。

また、たかだか6ステップ程度ではあるが、先の方法よりは回りくどい。
そうまでしてiOS12以前っぽい見た目にするのか?となるので、やはりiOS13標準に則ったUIに変更するのが真っ当だと思う。

参考

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

swiftでポップアップの値渡しを簡単に実装する方法

はいどうもこんにちは

相変わらずGithubがろくすっぽ使えませんが何とか生きています

いつもハマった話ばかり書いていますが、今日は別にハマっていないけど、デリゲートでコールバックするより綺麗だなと思ったので書きます!

デリゲートは沢山コールバックをするならひとまとめにできるのでわかりやすいのですが、一つコールバックしたいってくらいだとこっちのがシンプルで楽な気がします。

特にポップアップが閉じられた時にコールバックしたことってよくあると思うのですが、今回はそれです。

方針としては

1. ポップアップのコントローラに"メソッドの入れ物"を配置
2. ポップアップを開く前のコントローラに、ポップアップを開くときのコードと、ポップアップが閉じたあとの処理を実装
3. ポップアップのコントローラに閉じるボタンを押されたときの処理を実装

こんな流れです

それではまずは

1. ポップアップのコントローラに"メソッドの入れ物"を配置

メソッドの入れ物です。今はまだ空っぽです。
これを置いておくことで、渡されたメソッドをポップアップのクラスで実行することができます。

PopupVC.swift
class PopupVC: UIViewController {
    // メソッドの入れ物みたいなものです
    var emptyClosure: (() -> Void)?
}

2. ポップアップを開く前のコントローラに、ポップアップを開く前の処理と閉じた後にやりたい処理を実装

特筆すべき点はありませんが、ポップアップのVCの開き方はケースバイケースなのでよしなにお願いします。

SomeVC.swift
class SomeVC: UIViewController {
    func showPopup(){
        let pupupVC = ()何かしらの方法でポップアップのコントローラをここに
        // ここで入れ物にメソッドを渡します
        pupupVC.emptyClosure = {self.hogeMethod()}
        // (ここで何かしらの方法でポップアップを開く処理を書きます)
    }
    func hogeMethod(){
        // (ポップアップが消えたときにやりたい処理)
    }
}

3. ポップアップのコントローラに閉じるボタンを押されたときのコールバック処理を実装

既に2.で空だった入れ物にはメソッドが入りました。

そしてここでdismiss()のcompletionにて、閉じるときにSomeVCで定義したメソッドを発動します。

PopupVC.swift
class PopupVC:UIViewController {
    // 空の入れ物でしたが、既にこいつはメソッドをもっています
    var emptyClosure: (() -> Void)?

    @IBAction func closeBtnAction(_ sender: UIButton) {
        self.dismiss(animated: false, completion: {
            // UIViewControllerが閉じられると共に実行します
            self?.emptyClosure!()
        })
    }
}

こんな感じでシンプルにコールバック処理が書けました。

コールバックの量が多い場合はdelegateを使った方が保守性は高いかもしれませんが、簡単なものならこれでもいいですね

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

SwiftでもPythonみたいに文字列操作したい!

最近使い始めたSwiftでもPythonみたいに文字列操作したい!

SwiftでもPythonみたいに文字列のスライスしたいなぁ

ってことでStringを拡張してPythonの文字列操作メソッドを生やすライブラリを作ってみました。

SwiftyPyString

https://github.com/ChanTsune/SwiftyPyString

Swift5で動きます。
Unicode準拠 & Pythonのドキュメント準拠で作ってます。

インストール

CocoaPods、Carthage、SwiftPMの主要なパッケージマネージャ3つに対応させています。

CocoaPods

pod 'SwiftyPyString'

Carthage

github 'ChanTsune/SwiftyPyString'

SwiftPM

import PackageDescription

let package = Package(
    name: "YourProject",
    dependencies: [
        .Package(url: "https://github.com/ChanTsune/SwiftyPyString.git", from: "1.0.1")
    ]
)

文字列操作

添字アクセス

let str = "0123456789"
str[0]
// 0
str[-1]
// 9

Python準拠なので負の数を利用した後ろからのアクセスもサポートしています。

文字列のスライス

let str = "0123456789"
str[0,5]
// 01234
str[0,8,2]
// 0246
str[nil,nil,-1]
// 9876543210

やりたかったスライスです。

一度これに慣れると他の言語でも使いたくなるやつです。

(だからこれを作ったわけですが笑)

コロンだけの省略記法はnilで代用することにしました。

ちなみに同じ動作をPythonで書くと以下のようになります。

str = "0123456789"
str[0:5]
# 01234
str[0:8:2]
# 0246
str[::-1]
# 9876543210

文字列検索

// 先頭からの検索  
"123412312312345".find("123") // 0

// 開始位置を指定して検索
"123412312312345".find("123",start:2) // 4

// 終了位置を指定して検索
"123412312312345".find("123",end:1) // -1

// 末尾からの検索
"123412312312345".rfind("123") // 10

末尾からの検索も同様に開始位置と終了位置を指定して検索できます。

文字列結合

let array = ["abc","def","ghi"]
"".join(array) // "abcdefghi"
"-".join(array) // "abc-def-ghi"
"++".join(array) // "abc++def++ghi"

トリミング

// 右端のみ
"rstrip sample   ".rstrip() // "rstrip sample"
"rstrip sample   ".rstrip("sample ") // "rstri"
"  rstrip sample".rstrip() // "  rstrip sample"

// 左端のみ
"  lstrip sample".lstrip() // "lstrip sample"
"  lstrip sample".lstrip(" ls") // "trip sample"
"lstrip sample".lstrip() // "lstrip sample"

// 両端
"   spacious   ".strip() // "spacious"
"www.example.com".strip("cmowz.") // "example"

文字列分割

行ごとの分割
"abc\nabc".splitlines() // ["abc", "abc"]
"abc\r\nabc\n".splitlines() // ["abc", "abc"]

// 改行文字を残して分割
"abc\nabc\r".splitlines(true) // ["abc\n", "abc\r"]
"abc\r\nabc\n".splitlines(true) // ["abc\r\n", "abc\n"]
指定文字での分割
"a,b,c,d,".split(",") // ["a", "b", "c", "d", ""]

"aabbxxaabbaaddbb".split("aa") // ["", "bbxx", "bb", "ddbb"]

// 分割の回数を指定
"a,b,c,d,".split(",", maxsplit: 2) // ["a", "b", "c,d,"]

出現回数カウント

"abc abc abc".count("abc") // 3

// 開始位置の指定
"abc abc abc".count("abc", start:2) // 2

// 終了位置の指定
"abc abc abc".count("abc", end:1) // 0

ゼロ埋め

"abc".zfill(1) // "abc"
"abc".zfill(5) // "00abc"

// 符号付きの場合
"+12".zfill(5) // "+0012"
"-3".zfill(5) // "-0003"
"+12".zfill(2) // "+12"

符号付きの場合は符号の後ろにゼロが入ります。

さいごに

以上、簡単に主要な機能の説明をさせて頂きました。

紹介したメソッド以外にもPython3.7.3の時点で利用できるstr型のメソッドは言語機能的に実装出来ない、あるいは実装が難しいもの以外はほとんど実装してあります。

実装してあるメソッドの一覧は、こちらをご覧ください。
https://github.com/ChanTsune/SwiftyPyString/blob/master/README.md

一部、標準のメソッドと機能がかぶるものもありますが、PythonからSwiftに移植したいなんて言う事があれば多少は移植作業が楽になるのではないでしょうか?(普通にPythonを動くようにした方が多分楽 笑)

そうでなくともPythonからプログラミングを始めたという人なら、慣れ親しんだPythonの文字列操作ができるようになるので比較的便利ではないでしょうか?

このメソッド実装できるよ、とかこっちの実装の方がパフォーマンスいいんじゃない? Swiftだったらこう書くと綺麗だよ等ありましたら教えてください。

プルリクお待ちしております。

もしあれば、バグ報告とかも嬉しいです。

以前、C++版も作っているのでこちらも宜しければ
c++でもpythonのstr型のメソッドを使いたい!
https://qiita.com/ChanTsune/items/38814ca81738877c51fe

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