20200219のSwiftに関する記事は8件です。

[Swift5.1] Dynamic Member Lookup(KeyPath)によるProxyパターンの実装(プロパティのみ)

じょ

某Twitter上で「CGRectはCGFloatが面倒くさい」という旨の話が流れてきたので、いろいろ考えていたらピコーンときたので。

Proxyパターン

GoFのあれです。今回はインターフェースを変えてしまうパターンの奴です。

Dynamic Member Lookup(KeyPath)

Dynamic Member LookupはSwift4.2で実装されましたが、この時はKeyをStringでとるようになっていたため、Typoをしてもコンパイル時には検出できないという致命的な欠陥がありました。
しかし、Swift5.1でKeyPathをKeyにとる方式が実装されこれが克服されました。

やり方

目的はCGFloat型のプロパティをFloat型で扱えるようにすることです。

CGPoint

CGRectにはCGPoint型のプロパティもありますので、まずこちらを実装しちゃいます。

@dynamicMemberLookup
struct Point {
    var cgPoint: CGPoint

    subscript(dynamicMember keyPath: WritableKeyPath<CGPoint, CGFloat>) -> Float {
        get { Float(cgPoint[keyPath: keyPath]) }
        set { cgPoint[keyPath: keyPath] = CGFloat(newValue) }
    }
}

以上です。
CGPointにはプロパティが2つしかないので直接コンピューテッドプロパティとしてもいいのですが、Dynamic Member Lookup(KeyPath)を使った型変換Proxyが簡単にできるという例です。

ポイントはKeyPath(ここではWritableKeyPath)の型パラメータを明示してあげるというところです。
こうすることでこのsubscriptはCGPoint型のプロパティのうちCGFloatを返すKeyPath以外を受け付けなくなります。

これでCGPoint型であればFloatへのキャスト(あるいはFloatをCGFloatへ)する必要があったものが、そのままFloatとして利用可能になります。

var point = Point(cgPoint: .zero)
print(point.x)
// prints 0.0

point.x = Float(10)  // 例のためFloatを明示
print(point.cgPoint.x)
// prints 10.0

CGSize

CGPointと同じです!

@dynamicMemberLookup
struct Size {
    var cgSize: CGSize

    subscript(dynamicMember keyPath: WritableKeyPath<CGSize, CGFloat>) -> Float {
        get { Float(cgSize[keyPath: keyPath]) }
        set { cgSize[keyPath: keyPath] = CGFloat(newValue) }
    }
}

CGRect

最後にCGRectです。
これも同じといえば同じです。

@dynamicMemberLookup
struct Rect {
    var cgRect: CGRect

    subscript(dynamicMember keyPath: WritableKeyPath<CGRect, CGFloat>) -> Float {
        get { Float(cgRect[keyPath: keyPath]) }
        set { cgRect[keyPath: keyPath] = CGFloat(newValue) }
    }

    subscript(dynamicMember keyPath: WritableKeyPath<CGRect, CGPoint>) -> Point {
        get { Point(cgPoint: cgRect[keyPath: keyPath]) }
        set { cgRect[keyPath: keyPath] = newValue.cgPoint }
    }

    subscript(dynamicMember keyPath: WritableKeyPath<CGRect, CGSize>) -> Size {
        get { Size(cgSize: cgRect[keyPath: keyPath]) }
        set { cgRect[keyPath: keyPath] = newValue.cgSize }
    }
}

こちらはCGPointをPoint、CGSizeをSizeとして扱えるようにするインターフェイスの書き換えを追加しています。

多段のドットアクセスも可能です。

var rect = Rect(cgRect: CGRect(x: 0, y: 0, width: 100, height: 23))
rect.size.width = Float(120)  // 例のためFloatを明示
print(rect.cgRect)
// prints (0.0, 0.0, 120.0, 23.0)

問題点

変なエラー

readonlyなプロパティに代入を行おうとすると、ちょっと意味不明なエラーが出てしまいます。

var rect = Rect(cgRect: CGRect(x: 0, y: 0, width: 100, height: 23))

rect.height = 30  // ambiguous reference to member 'height'

これは「mutableなキーパスheightを探したけどよく分からない」的な意味合いだと思うのだが、突然これを出されると「heightプロパティ以外の何?? ほかにheightってあったっけ??」となること請け合いです。

メソッドどこ行った?

メソッドは頑張って書くしかないです。

まとめ

「CGRectのCGFloatをFloatにしたRectを作る」と見たときは一瞬簡単だと思ったんですけど、プロパティの数が無駄に多いことが簡単さレベルを大幅に下げてることに気づいた。
これがDynamic Member Lookup(KeyPath)でいとも簡単に解決して、これはほかでも使えるのではないだろうか、と感じこの記事を書いた次第です。
僕は思いついてないのですが、これを見た人がこれをヒントにすごい使い方を編み出してくれることを期待しています。

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

【swift】Mapkitに表示されているpinの画像を一律すべて変える

Mapkitのpinの画像をデフォルトのpin画像から、自分で選んだpinの画像に変更をするメモです。

1.storyboardにMapKitを貼る

storyboardにMapKitを貼ります。

2.Mapkitとコードを接続し、Mapkitの宣言をする。

weakで今回は名前を「pinChangeMap」にします。

@IBOutlet weak var pinChangeMap: MKMapView!

3.画像を登録する

左メニューにある「Assets.xassets」にpinの画像にしたい画像をドラッグし登録する。
今回名前は「mark1」にしました。

3.viewControllerにコードを書く

下記の通りコードを書きます。

import UIKit
import MapKit

 class ViewController: UIViewController ,MKMapViewDelegate{

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        // MapViewのdelegate通知先を設定
        sentouMap.delegate = self

        //MKPointAnnotationを宣言
        let annotation = MKPointAnnotation()
        // ピン(アノテーション)の緯度経度を指定
        annotation.coordinate = CLLocationCoordinate2DMake(35.681236, 139.767125)
        // アノテーションのタイトルを指定
        annotation.title = "東京駅"
        // アノテーションのサブタイトルを指定
        annotation.subtitle = "Tokyo Station"
        // mapにアノテーションを追加
        self.pinChangeMap.addAnnotation(annotation)
    }


    @IBOutlet weak var sentouMap: MKMapView!

    // mapViewのデリゲート
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {

        // MKPinAnnotationViewを宣言
        let annoView = MKPinAnnotationView()
        // MKPinAnnotationViewのannotationにMKAnnotationのAnnotationを追加
        annoView.annotation = annotation
        // ピンの画像を変更
        annoView.image = UIImage(named: "mark1")
        // 吹き出しを使用
        annoView.canShowCallout = true
        // 吹き出しにinfoボタンを表示
        annoView.rightCalloutAccessoryView = UIButton(type: UIButton.ButtonType.detailDisclosure)

        return annoView   
    }
}

mapViewのデリゲートメソッドに宣言をするMKPinAnnotationViewで、imageの中に登録をした画像の名前を記載します。

let annoView = MKPinAnnotationView()
annoView.image = UIImage(named: "mark1")

これで選択した画像でPinを作成することができました。

最後に

これで任意の画像をPinにすることができました。
ただ、上記コードだとMapkitで表示しているPinすべてを一律で変更してしまうため、Pinごとに異なった画像にすることができません。
(mapViewのデリゲートメソッドで画像を変えているため)

pinごとに異なった画像で表示するのは別記事で説明したいと思います。

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

今更聞けない「バージョン判定」

はじまり

さまざまなOSのバージョンアップで軒並みエラーとか苦しめられる時期が来るプログラミングの見直し時期はいつも、新しいコードのチェックとバーションアップした時の検証

見積書には、なかなか書けないかもしれないですが、それこそ見積もらなくてはいけないこのOSバージョンアップへの対応 いつまで経っても古いままだと、警告や使えなくなっていく機能などが増えてくる

iOSの悩みどころのこの時期にチェックしておきたい。
バージョン判定のお話です。

いつも忘れてしまうので、備忘録として、iOSのバージョン判定するには、以下の方法で行うようにしていきたいって言う提案もありますが、皆さんのご意見も含めここにまとめていこうと思います。

Objective-C 今のバージョンが知りたい場合 の例
Objective-C
// バージョン判定
[UIDevice currentDevice].systemVersion.floatValue >= 7;

OS version 7.0 以上と言うのが分かります。

Objective-C マクロを使って一行で書く方法 の例
Objective-C
#define SYSTEM_VERSION_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)

OS version の判定の時に使えそうです。
参考:マクロを使って一行で書く方法

Objective-C
float iOSVersion = [[[UIDevice currentDevice] systemVersion] floatValue];
if (iOSVersion > 9.9f) {
    // iOS10以降の場合
} else {
    // iOS9以前の場合
}

上記のように、iOSのバージョン判定で、直接値が書く方法でもこちらは、有効です。

iOS 8.0 で NSProcessInfo に追加されたメソッドを使用すると簡単にバージョンの判定が行える。

Objective-C
NSOperatingSystemVersion version = {8, 3, 0};
BOOL isOSVersion8_3Later = [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version];
if (isOSVersion8_3Later) {
    // iOS 8.3 以降
} else {
    // iOS 8.3 未満
}

上記のように、iOS のバージョンを簡単にマイナーバージョン等を加味して判定する方法もあるようですね。
参考:iOS のバージョンを簡単にマイナーバージョン等を加味して判定する方法

Objective-C
- (BOOL)isOS10 {
    BOOL isOS10 = NO;
    if ( floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max ) {
        // iOS10以降の場合
        isOS10 = YES;
    } else {
        // iOS9以前の場合
    }
    return isOS10;
}

上記のように、iOS のバージョンを判定する場合においては、こちらのisOSXX を用意しておくのも後々開発の時に便利かもですね。

理由としては、OSを全体にバージョンアップした場合は要らなくなるコードが増え、全体的なコストやリスクを回避できるからですね。

Objective-C
- (BOOL)isOS10Handler:(void (^)(void))iOSHandler otherHandler:(void (^)(void))otherHandler {
    BOOL isOS10 = NO;
    if ( floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max ) {
        // iOS10以降の場合
        if (iOSHandler) {
            iOSHandler();
        }
        isOS10 = YES;
    } else {
        // iOS9以前の場合
        if (otherHandler) {
            otherHandler();
        }
    }
    return isOS10;
}

上記のように、好みにもよりますが上記のハンドリングを用いておけば、判定と変更もメソッド内で個別に対応が出来たりします。

レイアウト調整の場合においてもですが、切り替える際にコードで書かなくてはならない時はこちらが便利です。
ブロック構文に関しては別の時に学んでいきます。

iOSのバージョンによって異なるデザイン(iOS7以前)やiOS10から変わったウィジェットの背景色などを調整したりする際にこの判定処理が使えます。
以下がその例です。

Objective-C 切り替える場合 の 例
Objective-C
if ( floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max ) {
    // iOS10以降の場合
} else {
    // iOS9以前の場合
}

iOS10以降の場合

Objective-C
if (@available(iOS 13.0, *)) {
    // iOS13以降の場合
} else {
    // iOS13未満の場合
}

iOS13以降とiOS12以前の場合で切り替えることが可能です。
特に、Popoverの時などのレイアウト変更の時はよく使いました。
こちらの苦労話は、またどこかで書きたいと思います。

Swift 切り替える場合 の 例
Swift
if floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max {
    // iOS10以降の場合
} else {
    // iOS9以前の場合
}

iOS10以降の場合

Swift
if #available(iOS 10.0, *) {
    // iOS10以降の場合
} else {
    // iOS9以前の場合
}

参考:iOS 7 UI Transition Guide

関連記事

  • マクロの書き方
  • Popover
  • ブロック構文
  • バーションアップ
  • Objective-C 一覧
  • Swfit 一覧

制作チーム:サンストライプ

http://sunstripe-main.jp/
(月1WEBコンテンツをリリースして便利な世の中を作っていくぞ!!ボランティアプログラマー/デザイナー/イラストレーター/その他クリエイター声優募集中!!)

地域情報 THEメディア

THE メディア 地域活性化をテーマに様々なリリース情報も含め、記事をお届けしてます!!
https://the.themedia.jp/

ゼロからはじめる演劇ワークショップ

多様化の時代に向けて他者理解を鍛える

https://workshop.themedia.jp/

プログラミングワークショップ・ウェブ塾の開講!!!

様々なテーマでプログラミングに囚われずに取り組んでいきます。
詳しくはこちら ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
プログラミングサロン 月1だけのプログラミング学習塾

協力応援 / 支援者の集い

トラストヒューマン

http://trusthuman.co.jp/
私たちは何よりも信頼、人と考えてます。

「コンサルティング」と「クリエイティブ」の両角度から「人材戦略パートナー」としてトータル的にサポートします!!

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

【Swift】モバイルでのJWT(JSON Web Token)の扱い方を考える

下記の記事を読んでいて
JWTについての知識や扱い方などを十分に把握できていないと感じ
まとめてみました。
https://tech.just-eat.com/2019/12/04/lessons-learned-from-handling-jwt-on-mobile/

JWTについて

JWTとは?

JSON Web Tokenの略で
RFC7519で定義されている
JSONをベースとしたアクセストークン※のためのオープン標準です。


アクセストークンはリソースに直接アクセスするために必要な情報を保持しています。
クライアントがリソースを管理するサーバにアクセストークンを渡すときに
サーバはそのトークンに含まれている情報を使用して
クライアントが認可したものかを判断します。

JWTの特徴

  • コンパクトな設計でURLセーフ
  • RFC7515RFC7516のどちらかに準拠する必要がある
  • 当事者の一方または両方の秘密鍵により署名されており、発行されたトークンが正規のものが確認可能
  • トークン内に任意の情報を保持可能
  • 有効期限があり、期限切れのトークンはリフレッシュする必要がある

JWTの構成

「ヘッダー」と「ペイロード」と「署名」で構成されます。

スクリーンショット 2020-02-17 14.32.30.png

最終的な形は
この3つの要素をbase64変換してドットで区切った形式になります。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

https://ja.wikipedia.org/wiki/JSON_Web_Token

有効期限

ペイロードにexpと呼ばれる標準フィールドがあり
この期限以降はトークンを受け付けられなくしなければなりません。

トークンのリフレッシュにまつわる問題

トークンのリフレッシュはどのアプリでも共通して扱われていますが
上手く実装しないとユーザが頻繁にログアウトしてしまうなどの問題が発生します。

例えば下記のような場合があります。

同時並行で2つのAPIを呼ぶ

すでにトークンが有効期限切れの状態で
2つのAPIを同時並行で呼び出した場合
2つのリクエストが競合状態になり

1回目のリクエストで
リフレッシュトークン(※)を使用してアクセストークンが更新されているのに

2回目のリクエストでは
アクセストークンの更新リクエストを呼び出した時点では最新であったものの
1回目のリクエストですでに古くなってしまったリフレッシュトークンを使用して
アクセストークンを更新しようとしているため

サーバがステータスコード401を返し
アプリはログアウトのような処理をしてしまう
という現象が起きます。

下記の図のようなイメージです。

jwt.png


リフレッシュトークンは
新しいアクセストークンを取得するために必要な情報を保
持しています。
特定リソースにアクセスする際にアクセストークンが必要な場合は
クライアントは認証サーバが発行する新しいアクセストークンを取得するために
リフレッシュトークンを使用します。

一般的にはアクセストークンの期限が切れた後に新しいものを取得したり
初めて新しいリソースにアクセスするときに使用します。

起動時でなくても
直前でトークンの有効期限が切れた場合も同様です。

時刻の同期が取れていない

クライアントとサーバで時刻がかなり離れている場合
クライアントでは有効なトークンを保持しているはずなのに
なぜか有効期限切れと判定されてしまいます。

これは
原因が特定しづらいとても複雑な状態を生み出します。

よくある処理はちょっとコストが高い

よく見られる処理方法として

APIをリクエストしてみて
ステータスコード401が返却されたらトークンをリフレッシュする

というものですが

こうすると
すでに期限切れのアクセストークンを使用した
リクエストをしてしまうことに加え
呼び出し回数が増えたことで
リクエスト間の競合状態を生み出す可能性も高めます。

対処方法

上記のような余計なリクエストを減らすために
下記のような方法が挙げられます。

  • ローカルのアクセストークンの有効期限をチェックして有効ならばそのまま使用する。
  • 無効ならばアクセストークンをリフレッシュする。その際に1度に1つのリクエストしかサーバに送れないように制御をする。

実装

では具体的にどういう処理が必要になるのか?

まずは処理の流れを見てみます。

jwt-Page-2.png

上記のJWT取得プロセスのところにスレッドの制御を加えます。

Grand Central Dispatch (GCD)のDispatchQueueDispatchSemaphoreを用います。
https://developer.apple.com/documentation/DISPATCH

typealias Token = String
typealias AuthValue = Token

struct AuthInfo {
    let accessToken: Token
    let refreshToken: Token
    let expiredDate: Date
    var isValid: Bool {
        expiredDate.compare(Date()) == .orderedDescending
    }
}

enum AuthError: Error {
    case missingAuthInfo
    case clientError

    var isClientError: Bool {
        if case .clientError = self {
            return true
        }
        return false
    }
}

protocol TokenRefreshing {
    func refreshAccessToken(_ refreshToken: Token, completion: @escaping (Result<AuthInfo, AuthError>) -> Void)
}

protocol AuthStore {
    var authInfo: AuthInfo? { get }
    func save(authInfo: AuthInfo)
    func clearAuthInfo()
}

class AuthProvider {
    private let tokenRefreshingAPI: TokenRefreshing
    private let store: AuthStore

    private let queue = DispatchQueue(label: "AuthProvider", qos: .userInteractive)
    private let semaphore = DispatchSemaphore(value: 1)

    init(tokenRefreshingAPI: TokenRefreshing, store: AuthStore) {
        self.tokenRefreshingAPI = tokenRefreshingAPI
        self.store = store
    }

    func getValidAuthInfo(completion: @escaping (Result<AuthValue, Error>) -> Void) {
        queue.async { [weak self] in
            self?.getValidAuthInfoInMutualExclusion(completion: completion)
        }
    }

    private func getValidAuthInfoInMutualExclusion(completion: @escaping (Result<AuthValue, Error>) -> Void) {
        // 以下の処理へのアクセスを1度に1つだけにする
        semaphore.wait()

        // ローカルにAuthInfoがあるかどうかを確認する
        guard let authInfo = store.authInfo else {
            semaphore.signal()
            completion(.failure(AuthError.missingAuthInfo))
            return
        }

        // ローカルのAuthInfoが有効かどうかを確認する
        if authInfo.isValid {
            self.semaphore.signal()
            completion(.success(authInfo.accessToken))
            return
        }

        // リモートからAuthInfoを取得する
        tokenRefreshingAPI.refreshAccessToken(authInfo.refreshToken) { [weak self] result in
            guard let self = self else {
                return
            }
            switch result {
            case .success(let authInfo):
                self.store.save(authInfo: authInfo)
                self.semaphore.signal()
                completion(.success(authInfo.accessToken))
            case .failure(let error) where error.isClientError:
                self.store.clearAuthInfo()
                self.semaphore.signal()
                completion(.failure(error))
            case .failure(let error):
                self.semaphore.signal()
                completion(.failure(error))
            }
        }
    }
}

認証が必要なAPIリクエストは
AuthProviderからAuthValue(今回の場合はToken)を取得し必要な箇所に設定します。
(多くの場合はヘッダーにAuthorization: Bearer <jwt-token>と設定するかと思います。)
AuthProviderはアプリ全体で同じqueueを使用するためにclassにして参照を共有するようにします。

getValidAuthInfoでは
シリアルなDispatchQueueを使用してメソッド呼び出しの順番を制御しています。

getValidAuthInfoInMutualExclusionはDispatchSemaphoreを使用しています。

これは内部でトークンをリフレッシュする非同期のAPIを呼び出しており
仮にこのリフレッシュ処理に時間がかかってしまうと
次のリクエストのリフレッシュAPIが先に呼び出されてしまう場合があります。

もう少し詳細に言うと
仮にDispatchSemaphoreでの制御がない場合
tokenRefreshingAPI.refreshAccessTokenはasyncなので
コールバックが返ってくる前にgetValidAuthInfoInMutualExclusionメソッドを抜けます。

そうするとgetValidAuthInfoのqueue.asyncのブロックを抜けて
次のリクエストによるgetValidAuthInfoInMutualExclusionが呼ばれて
中のAPIも呼ばれてしまいます。

これをDispatchSemaphoreのvalueを1にすることで
semaphore.waitを呼んでから
semaphore.signalが呼ばれるまでの処理を
1度に1つのスレッドしかアクセスできないようにしています。

AuthInfoをクリアするのには注意が必要で
例えば他のデバイスでパスワードの変更をおこなっていて
すでに無効なリフレッシュトークンを使ってリクエストを送っていた場合など
認証情報が無効であるときに限定しています。

単純なエラーの場合にクリアしてしまうと
通信が失敗したら突然ログアウトをしてしまった
のような印象を持たれてしまうかもしれません。

JWTのペイロードを正しくパースする方法

たいていの場合は問題ないものの
base64へのエンコード処理の中には
paddingの文字が必要ないものがある一方で
Foundationでは必須となっています。

StackOverFlowでも↓のような回答があります。
https://stackoverflow.com/questions/36364324/swift-base64-decoding-returns-nil/36366421#36366421

これに対処するためには下記のような置換が必要になります。
https://github.com/kylef/JSONWebToken.swift/blob/master/Sources/JWT/Base64.swift

RFCにも記載があります。
https://tools.ietf.org/html/rfc7515#appendix-C

複数のJWTを使用する

例えば
ログインユーザと匿名ユーザ(ログインしていないで利用しているユーザ)で
異なるJWTを使ってAPIのリクエストをする場合があるとします。

これを
AnonymousJWT(匿名ユーザ用JWT)
UserJWT(ユーザ用JWT)
とします。

初めてアプリを使用するユーザが

一番最初のAPIを呼び出した時に
AnonymousJWTを取得してローカルに保存し

ログイン時に
UserJWTを取得してローカルに保存するとします。

この時に考えられるユーザの状態として3パターン考えられます。

未認証
匿名認証済
ユーザ認証済

図にしてみると下記のようなイメージになります。

jwt-Page-3.png

このような場合に

  • UserJWTがある場合はUserJWTを利用するとレスポンスでユーザに合わせてカスタムしやすい
  • ログアウト時に有効なAnonymousJWTを残しておけばAnonymousJWTを再取得する処理を省ける

など
より効果的に使用することもできます。

まとめ

JWTについて見てみました。

普段はFirebaseに任せていることが多かったので
詳細な中身を見ていくことで
単純にパースができないJWTがあるなど
色々な発見がありました。

また
トークンのリフレッシュ処理をきちんと制御しないと
予期せぬ動作をさせてしまう可能性がある点は
十分に気をつけないといけないなと感じました。

今回はJWTの処理がテーマでしたが
非同期処理による競合状態を起こしてしまうことは
他でも起こりうると思いますので
意識しておくとよいかもしれませんね?

何か間違いなどございましたらご指摘いただけますとうれしいです??‍♂️

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

UIViewに角丸と影をつける方法について

はじめに(とばしても構いません?)

初めましてりゅーちゃんです!!
今回はUIViewに角丸、影をつける際に困ったことについて共有していきます?‍♂️
参考に慣れば幸いです!!
業務で実装する際に詰まってしまったので自分の備蓄として書いていきます!!
もっといい方法があるよという方がいたらコメントしていただけると幸いです?

完成画面

スクリーンショット 2020-02-19 1.36.58.png
この画面を生成していきたいと思います!
それではコードの方を見ていきましょう!!?

実装コード

qiita.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var sampleView: UIView!
    @IBOutlet weak var shadowView: UIView!

    override func viewDidLoad() {:helmet_with_cross:
        super.viewDidLoad()

        setUpsampleView()
    }

    private func setUpsampleView() {
        let viewRadius: CGFloat = 20.0
        shadowView.layer.cornerRadius = viewRadius
        shadowView.layer.shadowColor = UIColor.red.cgColor
        shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
        shadowView.layer.shadowRadius = 7
        shadowView.layer.shadowOpacity = 1

        sampleView.layer.cornerRadius = viewRadius
        sampleView.layer.masksToBounds = true
    }
}

実装のためのコードはこちらになります:eyes:

ストーリーボード

スクリーンショット 2020-02-19 1.42.15.png
ストーリーボード上でのUIViewの配置は画像のようになります!
sampleViewのしたにshadowViewを作成し影、角丸を表示させています:relaxed:


ちょっと待った!!

View1つに対して実装できないのか?!と思う方がいるかもしれません:frowning2:

僕も思いました。。。。
だからやって見たんですが。。。

qiita.swift
mport UIKit

class ViewController: UIViewController {

    @IBOutlet weak var sampleView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        setUpsampleView()
    }

    private func setUpsampleView() {
        let viewRadius: CGFloat = 20.0
        sampleView.layer.cornerRadius = viewRadius
        sampleView.layer.shadowColor = UIColor.red.cgColor
        sampleView.layer.shadowOffset = CGSize(width: 0, height: 0)
        sampleView.layer.shadowRadius = 7
        sampleView.layer.shadowOpacity = 1

        sampleView.layer.cornerRadius = viewRadius
        sampleView.layer.masksToBounds = true
    }
}

これで完璧だ!!完全に理解した!!と思っていたのですが。。。

スクリーンショット 2020-02-19 2.07.08.png

なんでやねん!!なんでできへんのやて!!となりました。。。

同じViewに対しては角丸、影をつけることはできないらしいです(調べた記事に関してはQiita外の記事になりますので今回は記載するのはやめておきます:bow_tone1:
(検索ワード:How to implement rounded corner image view with shadowで検索してみてください!)

最後に

今回はUIViewに角丸、影をつける処理に関して記載していきました!
ただこれだとView1つ、1つに処理を書かなければいけないのでextensionなどを使うのがいいと思います。。。
この話題に関しては、今回の記事を更新して付け加えておきます。。。
遅かったら催促してください!!:muscle:
ここまで読んでいただいてありがとうございました!
少しでもお役に立てると幸いです!!!!!

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

Swift:勝手にメニューに追加される「音声入力を開始」と「絵文字と記号」を除く方法

macOSアプリ開発でNSTextViewNSTextFieldを使っていて、メニューに余計なものが勝手に追加されることに気づきました。「音声入力を開始」と「絵文字と記号」というやつです。
スクリーンショット 2020-02-19 0.59.29.png
これを取り除く方法を記しておきます。

AppDelegate.swift
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationWillFinishLaunching(_ notification: Notification) {
        UserDefaults.standard.set(true, forKey: "NSDisabledDictationMenuItem")
        UserDefaults.standard.set(true, forKey: "NSDisabledCharacterPaletteMenuItem")
        UserDefaults.standard.synchronize()
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {

    }

}

applicationWillFinishLaunchingの内部でUserDefaultsを介して設定をいじります。DidではなくWillなところがポイントです。

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

【Swift】実機テスト時、アプリを削除したのに「最大数に達しました。」エラーが発生した時の対処法

実機テストを行おうとしたところ、以下のエラーが発生。

App installation failed The maximum number of apps for free development profiles has been reached.

Google先生に訳してもらうと「アプリのインストールに失敗しました無料の開発プロファイルのアプリの最大数に達しました。」とのこと。

ならばと実機に溜まっていた開発中アプリを削除し再度インストールしようとしたが、また同じエラーが発生。

そんなときに行った対処法を紹介します。

環境
 macOS:10.15.3
 Xcode:11.3.1
 Swift:5.1.3

まず、開発中アプリが3つ、実機にインストールされた状態で
image.png

新たにアプリをインストールしようとすると
image.png

エラーが発生しました。
Details(詳細)を確認すると上記の「最大数に達しました。」のエラーが表示されています。
ちなみに、Xcodeのメニューバー [Window] から [Device and Simulators] を選択すると実機にインストールされているアプリの一覧が見れました。
image.png

このまま、実機がMacにつながっている状態で開発中アプリを一つ削除。
image.png

Xcodeのアプリ一覧からも削除されました。
image.png

このあとに新たなアプリのインストールした場合は正常にインストールされました。
image.png

問題は、実機をMacから切断した状態で開発中アプリを削除した場合です。

現在、実機には開発中アプリが一つしかインストールされていません。
image.png

この状態で新たなアプリのインストールしようとすると
image.png

エラーが発生しました。
Details(詳細)には、アプリを削除しているにもかかわらず「最大数に達しました。」のエラーが表示されています。
アプリ一覧にも、インストール中のアプリは一つしか表示されていません。
image.png

このままでは、新たにアプリをインストールし実機テストを行うことができません。
そこで、以下の操作を行いました。

このアプリ一覧の [Connected] 欄の空きスペースをControlキーを押したままクリック(もしくは副ボタンクリック、右クリック)
表示された項目から [Show Provisioning Profiles...] を選択。
image.png

新たに表示された画面には、削除したアプリが残っていました。
image.png

ここから不要なアプリ選択し [-] を押下して削除。[Done] を押下します。
image.png

その後、新たにアプリをインストールしたところ、実機へ正常にインストールできました。
image.png

以上、「最大数に達しました。」エラーが発生した際にお試しください。

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

【Swift】AirtableとMoyaを使ったAPI通信

はじめに

VUI関係の登壇に伴い、スマートスピーカーとiOSアプリ何かで組み合わせたものを簡単に作りたいと思い、Airtableを見つけました。
アプリに取り入れようと思ったところ、Airtableの使い方に関する記事が少なかったため備忘録も兼ねて記事にしました。

・Moya: https://github.com/Moya/Moya (Airtableとの通信用に利用)

・Airtableとは
簡単操作でウェブ上にデータベースを作成して視覚的に操作もできる「Airtable」を使ってみた

何を作るか

自分が大好きなファミレス、デニーズで何曜日にどこで何を食べたかを記録するアプリ
記録したものを参考にスマートスピーカーがメニューをおすすめ(今回の記事では紹介割愛)

Airtable準備

今回は初回の準備からTable作成・ドキュメント確認まで説明を記載致します。

Airtable公式サイトで右上のSign upを押下し必要事項を入力し登録を行う

スクリーンショット 2020-02-16 16.23.17.png

登録後再度サイトを開くとBasesが開かれる。
Workspaceが既にありますが、使い分けるためにWorkspaceを新しく作成したい場合はAdd a workspaceを押下すると新しいWorkspaceが作成できます、(作らなくても可)

下記添付画像のような画面になったら Add a base を押下
今回はスマートスピーカー側のAirtableのテストも兼ねており、初期データを楽にいれたかったためcsvを用意していました。
そのため import a spreadsheetを押下
(CSVを用意していない場合は Start from scratch からぽちぽちTable作れます)

スクリーンショット 2020-02-16 16.27.42.png

Choose a .CSV file を押下し、下記のcsvを選択

スクリーンショット 2020-02-16 16.27.59.png

Dennys.csv
type,name,week,status
ご飯,とろ〜り卵とチーズのオムライス,日,普通
肉,大盛りカットステーキ,月,空いてる
肉,トロけるお肉のビーフシチュー,火,空いてる
パスタ,熟成卵黄と4種チーズのカルボナーラ,水,普通
ご飯,オマール香るエビドリア,木,普通
デザート,デビルズブラウニーサンデー,金,空いてない
デザート,キャラメルハニーパンケーキ,土,空いてない

CSVをいれたあとにTable名を変更したり、項目に間違いがないか確認
(私は店名の項目の入れ忘れに気付いてCSV用意したのに結局ここの画面から項目増やしました…)
スクリーンショット 2020-02-16 16.41.14.png

テーブルの用意ができたらAPIを呼び出すためのドキュメントを確認します。
右上のHELP を押下しAPI documentation押下

スクリーンショット 2020-02-16 16.51.58.png

APIの使用例がとても分かりやすく書いてあります。
Airtableアカウント画面でAPIkeyを作成していない方は、AUTHENTICATIONの項目のaccountを押下しアカウント画面でAPIkeyを作成してください。

スクリーンショット 2020-02-16 16.56.20.png

今回は用途として、記録をするアプリのためCreate recordsを押下しレコードを登録するための情報を確認します。
スクリーンショット 2020-02-16 17.03.49.png

requestに必要な情報やサンプルresponseもあるため、ここを見るところまできたらアプリに組み込む準備はばっちりです。

Xcode準備

Cocoapodsを利用してMoyaをいれる。

pod 'Moya', '~> 13.0'

APIの定義

YOUR_TABLEは自身のTableURL(Create records 押下時右側記載のURL)
YOUR_API_KEYは自身のAPIkeyをいれます。

RecordDennysAPI.swift
enum RecordDennysAPI {
    case PostRecordDennys(type: String, name: String, week: String, status: String, store: String)
}

extension RecordDennysAPI: TargetType {

    // ベースURL
    var baseURL: URL {
        return URL(string: "https://api.airtable.com/v0/YOUR_TABLE")!
    }

    // パス
    var path: String {
        switch self {
        case .PostRecordDennys:
            return ""
        }
    }

    // メソッド
    var method: Moya.Method {
        return .post
    }

    // スタブデータ
    var sampleData: Data {
        var path = ""
        switch self {
        case .PostRecordDennys:
            path = Bundle.main.path(forResource: "SuccessRecordDennys", ofType: "json")!
        }
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }

    // リクエストパラメータ
    var task: Task {
        switch self {
        case .PostRecordDennys(let type,let name,let week,let status,let store):
            let request = PostDennysRecordsRequest(records: [PostDennysRecordsRequest.Fields(fields: PostDennysRecordsRequest.Fields.FieldsElement(type: type, name: name, week: week, status: status, store: store))])
            return .requestJSONEncodable(request)

        }
    }

    // ヘッダー
    var headers: [String : String]? {
        return ["Content-Type": "application/json",
                "Authorization": "Bearer YOUR_API_KEY"]
    }
}

エンコード・デコード用にEncodable・Decodableに準拠したstructを定義。
Create recordsで確認したrequestやresponseを確認しつつ作成。

struct  PostDennysRecordsRequest: Encodable {
    struct  Fields: Encodable {
        struct  FieldsElement: Encodable {
            let type: String
            let name: String
            let week: String
            let status: String
            let store: String
        }
        let fields: FieldsElement
    }
    let records: [Fields]
}

struct  DennysRecordsResponse: Decodable {
    struct  Fields: Decodable {
        struct  FieldsElement: Decodable {
            let type: String
            let name: String
            let week: String
            let status: String
            let store: String
        }
        let id: String
        let fields: FieldsElement
        let createdTime: String
    }
    let records: [Fields]
}

登録項目の入力準備

見た目にはあまりこだわらず、とりあえず店名と料理名以外は常に表示している下部のpickerで選ぶように実装
(本来ならinputviewとかででいい感じにした方が良いと思いますが…)

スクリーンショット 2020-02-16 17.26.44.png

ViewController.swift
    @IBOutlet weak var storeTextField: UITextField!
    @IBOutlet weak var weekTextField: UITextField!
    @IBOutlet weak var dishTypeTextField: UITextField!
    @IBOutlet weak var dishNameTextField: UITextField!
    @IBOutlet weak var stomachStatusTextField: UITextField!
    @IBOutlet weak var choicePickerView: UIPickerView!

    // picker用
    let weekList = ["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"]
    let dishTypeList = ["肉","デザート","麺","ドリア","サイドメニュー"]
    let stomachStatusList = ["お腹いっぱい","普通","空いている"]
    var currentSelectedList = [""]
    var currentSelectedTextField = UITextField()

    override func viewDidLoad() {
        super.viewDidLoad()

        choicePickerView.delegate = self
        choicePickerView.dataSource = self

        // キーボードを非表示にするため
        weekTextField.inputView = UIView()
        dishTypeTextField.inputView = UIView()
        stomachStatusTextField.inputView = UIView()
    }

    /// 曜日TextFieldタップ時
    /// - Parameter sender: UITextField
    @IBAction func tapWeekTextField(_ sender: UITextField) {
        currentSelectedList = weekList
        currentSelectedTextField = weekTextField
        choicePickerView.reloadAllComponents()
    }

    /// 料理タイプTextFieldタップ時
    /// - Parameter sender: UITextField
    @IBAction func tapDishTextField(_ sender: UITextField) {
        currentSelectedList = dishTypeList
        currentSelectedTextField = dishTypeTextField
        choicePickerView.reloadAllComponents()
    }

    /// お腹の具合TextFieldタップ時
    /// - Parameter sender: UITextField
    @IBAction func tapStomachStatusTextField(_ sender: UITextField) {
        currentSelectedList = stomachStatusList
        currentSelectedTextField = stomachStatusTextField
        choicePickerView.reloadAllComponents()
    }

    // キーボード非表示用
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }

// pickerの設定
extension ViewController: UIPickerViewDataSource, UIPickerViewDelegate  {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return currentSelectedList.count
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return currentSelectedList[row]
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        currentSelectedTextField.text = currentSelectedList[row]
    }
}


API通信の呼び出し

MoyaProviderのインスタンスを作成

    let provider = MoyaProvider<RecordDennysAPI>()

登録ボタンタップ時に先ほど作成したPostRecordDennys呼び出す

    /// 登録ボタンタップ時
    /// - Parameter sender: UIButton
    @IBAction func tapRegisterButton(_ sender: UIButton) {

        let dishType = dishTypeTextField.text ?? ""
        let dishName = dishNameTextField.text ?? ""
        let week = weekTextField.text ?? ""
        let stomachStatus = stomachStatusTextField.text ?? ""
        let store = storeTextField.text ?? ""

        provider.request(.PostRecordDennys(type: dishType,
                                           name: dishName,
                                           week: week,
                                           status: stomachStatus,
                                           store: store)) { (result) in

            switch result {
            case let .success(response):
                let decoder = JSONDecoder()

                do {
                    let recodeResult = try decoder.decode(DennysRecordsResponse.self, from: response.data)
                    print(recodeResult)
                    //アラート表示
                    self.alert(name: recodeResult.records[0].fields.name)
                } catch let error {
                    print(error)
                }
            case  let .failure(error):
                print(error)
                break
            }   
        }
    }

    // 登録完了アラート
    func alert(name: String) {
        let alertController = UIAlertController(title: "デニーズ記録",
                                                message: name + "の登録が完了しました",
                                                preferredStyle: .alert)
        alertController.addAction(UIAlertAction(title: "OK",
                                                style: .default,
                                                handler: nil))
        present(alertController, animated: true)
    }

以上で単純なレコードの作成が行えます。
スマートスピーカー開発のためにAirtableに触れましたが、
MoyaでのRESTAPI実装経験も少なかったため、良い勉強になりました。

参考

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