- 投稿日:2020-02-19T20:34:33+09:00
[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.0CGSize
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)でいとも簡単に解決して、これはほかでも使えるのではないだろうか、と感じこの記事を書いた次第です。
僕は思いついてないのですが、これを見た人がこれをヒントにすごい使い方を編み出してくれることを期待しています。
- 投稿日:2020-02-19T20:02:03+09:00
【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ごとに異なった画像で表示するのは別記事で説明したいと思います。
- 投稿日:2020-02-19T11:46:33+09:00
今更聞けない「バージョン判定」
はじまり
さまざまな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-Cfloat iOSVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; if (iOSVersion > 9.9f) { // iOS10以降の場合 } else { // iOS9以前の場合 }上記のように、iOSのバージョン判定で、直接値が書く方法でもこちらは、有効です。
iOS 8.0 で NSProcessInfo に追加されたメソッドを使用すると簡単にバージョンの判定が行える。
Objective-CNSOperatingSystemVersion 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-Cif ( floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max ) { // iOS10以降の場合 } else { // iOS9以前の場合 }iOS10以降の場合
Objective-Cif (@available(iOS 13.0, *)) { // iOS13以降の場合 } else { // iOS13未満の場合 }iOS13以降とiOS12以前の場合で切り替えることが可能です。
特に、Popoverの時などのレイアウト変更の時はよく使いました。
こちらの苦労話は、またどこかで書きたいと思います。Swift 切り替える場合 の 例
Swiftif floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max { // iOS10以降の場合 } else { // iOS9以前の場合 }iOS10以降の場合
Swiftif #available(iOS 10.0, *) { // iOS10以降の場合 } else { // iOS9以前の場合 }関連記事
- マクロの書き方
- Popover
- ブロック構文
- バーションアップ
- Objective-C 一覧
- Swfit 一覧
制作チーム:サンストライプ
http://sunstripe-main.jp/
(月1WEBコンテンツをリリースして便利な世の中を作っていくぞ!!ボランティアプログラマー/デザイナー/イラストレーター/その他クリエイター声優募集中!!)地域情報 THEメディア
THE メディア 地域活性化をテーマに様々なリリース情報も含め、記事をお届けしてます!!
https://the.themedia.jp/ゼロからはじめる演劇ワークショップ
多様化の時代に向けて他者理解を鍛える
プログラミングワークショップ・ウェブ塾の開講!!!
様々なテーマでプログラミングに囚われずに取り組んでいきます。
詳しくはこちら ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
プログラミングサロン 月1だけのプログラミング学習塾協力応援 / 支援者の集い
トラストヒューマン
http://trusthuman.co.jp/
私たちは何よりも信頼、人と考えてます。「コンサルティング」と「クリエイティブ」の両角度から「人材戦略パートナー」としてトータル的にサポートします!!
- 投稿日:2020-02-19T08:04:28+09:00
【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セーフ
- RFC7515とRFC7516のどちらかに準拠する必要がある
- 当事者の一方または両方の秘密鍵により署名されており、発行されたトークンが正規のものが確認可能
- トークン内に任意の情報を保持可能
- 有効期限があり、期限切れのトークンはリフレッシュする必要がある
JWTの構成
「ヘッダー」と「ペイロード」と「署名」で構成されます。
最終的な形は
この3つの要素をbase64変換してドットで区切った形式になります。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHIhttps://ja.wikipedia.org/wiki/JSON_Web_Token
有効期限
ペイロードに
exp
と呼ばれる標準フィールドがあり
この期限以降はトークンを受け付けられなくしなければなりません。トークンのリフレッシュにまつわる問題
トークンのリフレッシュはどのアプリでも共通して扱われていますが
上手く実装しないとユーザが頻繁にログアウトしてしまうなどの問題が発生します。例えば下記のような場合があります。
同時並行で2つのAPIを呼ぶ
すでにトークンが有効期限切れの状態で
2つのAPIを同時並行で呼び出した場合
2つのリクエストが競合状態になり1回目のリクエストで
リフレッシュトークン(※)を使用してアクセストークンが更新されているのに2回目のリクエストでは
アクセストークンの更新リクエストを呼び出した時点では最新であったものの
1回目のリクエストですでに古くなってしまったリフレッシュトークンを使用して
アクセストークンを更新しようとしているためサーバがステータスコード401を返し
アプリはログアウトのような処理をしてしまう
という現象が起きます。下記の図のようなイメージです。
※
リフレッシュトークンは
新しいアクセストークンを取得するために必要な情報を保
持しています。
特定リソースにアクセスする際にアクセストークンが必要な場合は
クライアントは認証サーバが発行する新しいアクセストークンを取得するために
リフレッシュトークンを使用します。一般的にはアクセストークンの期限が切れた後に新しいものを取得したり
初めて新しいリソースにアクセスするときに使用します。起動時でなくても
直前でトークンの有効期限が切れた場合も同様です。時刻の同期が取れていない
クライアントとサーバで時刻がかなり離れている場合
クライアントでは有効なトークンを保持しているはずなのに
なぜか有効期限切れと判定されてしまいます。これは
原因が特定しづらいとても複雑な状態を生み出します。よくある処理はちょっとコストが高い
よく見られる処理方法として
APIをリクエストしてみて
ステータスコード401が返却されたらトークンをリフレッシュするというものですが
こうすると
すでに期限切れのアクセストークンを使用した
リクエストをしてしまうことに加え
呼び出し回数が増えたことで
リクエスト間の競合状態を生み出す可能性も高めます。対処方法
上記のような余計なリクエストを減らすために
下記のような方法が挙げられます。
- ローカルのアクセストークンの有効期限をチェックして有効ならばそのまま使用する。
- 無効ならばアクセストークンをリフレッシュする。その際に1度に1つのリクエストしかサーバに送れないように制御をする。
実装
では具体的にどういう処理が必要になるのか?
まずは処理の流れを見てみます。
上記のJWT取得プロセスのところにスレッドの制御を加えます。
Grand Central Dispatch (GCD)の
DispatchQueue
とDispatchSemaphore
を用います。
https://developer.apple.com/documentation/DISPATCHtypealias 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.swiftRFCにも記載があります。
https://tools.ietf.org/html/rfc7515#appendix-C複数のJWTを使用する
例えば
ログインユーザと匿名ユーザ(ログインしていないで利用しているユーザ)で
異なるJWTを使ってAPIのリクエストをする場合があるとします。これを
AnonymousJWT(匿名ユーザ用JWT)
UserJWT(ユーザ用JWT)
とします。初めてアプリを使用するユーザが
一番最初のAPIを呼び出した時に
AnonymousJWTを取得してローカルに保存しログイン時に
UserJWTを取得してローカルに保存するとします。この時に考えられるユーザの状態として3パターン考えられます。
未認証
匿名認証済
ユーザ認証済図にしてみると下記のようなイメージになります。
このような場合に
- UserJWTがある場合はUserJWTを利用するとレスポンスでユーザに合わせてカスタムしやすい
- ログアウト時に有効なAnonymousJWTを残しておけばAnonymousJWTを再取得する処理を省ける
など
より効果的に使用することもできます。まとめ
JWTについて見てみました。
普段はFirebaseに任せていることが多かったので
詳細な中身を見ていくことで
単純にパースができないJWTがあるなど
色々な発見がありました。また
トークンのリフレッシュ処理をきちんと制御しないと
予期せぬ動作をさせてしまう可能性がある点は
十分に気をつけないといけないなと感じました。今回はJWTの処理がテーマでしたが
非同期処理による競合状態を起こしてしまうことは
他でも起こりうると思いますので
意識しておくとよいかもしれませんね?何か間違いなどございましたらご指摘いただけますとうれしいです??♂️
- 投稿日:2020-02-19T02:21:55+09:00
UIViewに角丸と影をつける方法について
はじめに(とばしても構いません?)
初めましてりゅーちゃんです!!
今回はUIViewに角丸、影をつける際に困ったことについて共有していきます?♂️
参考に慣れば幸いです!!
業務で実装する際に詰まってしまったので自分の備蓄として書いていきます!!
もっといい方法があるよという方がいたらコメントしていただけると幸いです?完成画面
この画面を生成していきたいと思います!
それではコードの方を見ていきましょう!!?実装コード
qiita.swiftimport 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 } }実装のためのコードはこちらになります
ストーリーボード
ストーリーボード上でのUIViewの配置は画像のようになります!
sampleViewのしたにshadowViewを作成し影、角丸を表示させています
ちょっと待った!!
View1つに対して実装できないのか?!と思う方がいるかもしれません
僕も思いました。。。。
だからやって見たんですが。。。qiita.swiftmport 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 } }これで完璧だ!!完全に理解した!!と思っていたのですが。。。
なんでやねん!!なんでできへんのやて!!となりました。。。
同じViewに対しては角丸、影をつけることはできないらしいです(調べた記事に関してはQiita外の記事になりますので今回は記載するのはやめておきます)
(検索ワード:How to implement rounded corner image view with shadowで検索してみてください!)最後に
今回はUIViewに角丸、影をつける処理に関して記載していきました!
ただこれだとView1つ、1つに処理を書かなければいけないのでextensionなどを使うのがいいと思います。。。
この話題に関しては、今回の記事を更新して付け加えておきます。。。
遅かったら催促してください!!
ここまで読んでいただいてありがとうございました!
少しでもお役に立てると幸いです!!!!!
- 投稿日:2020-02-19T01:07:25+09:00
Swift:勝手にメニューに追加される「音声入力を開始」と「絵文字と記号」を除く方法
macOSアプリ開発で
NSTextView
やNSTextField
を使っていて、メニューに余計なものが勝手に追加されることに気づきました。「音声入力を開始」と「絵文字と記号」というやつです。
これを取り除く方法を記しておきます。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
なところがポイントです。
- 投稿日:2020-02-19T01:04:55+09:00
【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エラーが発生しました。
Details(詳細)を確認すると上記の「最大数に達しました。」のエラーが表示されています。
ちなみに、Xcodeのメニューバー [Window] から [Device and Simulators] を選択すると実機にインストールされているアプリの一覧が見れました。
このまま、実機がMacにつながっている状態で開発中アプリを一つ削除。
このあとに新たなアプリのインストールした場合は正常にインストールされました。
問題は、実機をMacから切断した状態で開発中アプリを削除した場合です。
現在、実機には開発中アプリが一つしかインストールされていません。
エラーが発生しました。
Details(詳細)には、アプリを削除しているにもかかわらず「最大数に達しました。」のエラーが表示されています。
アプリ一覧にも、インストール中のアプリは一つしか表示されていません。
このままでは、新たにアプリをインストールし実機テストを行うことができません。
そこで、以下の操作を行いました。このアプリ一覧の [Connected] 欄の空きスペースをControlキーを押したままクリック(もしくは副ボタンクリック、右クリック)
表示された項目から [Show Provisioning Profiles...] を選択。
ここから不要なアプリ選択し [-] を押下して削除。[Done] を押下します。
その後、新たにアプリをインストールしたところ、実機へ正常にインストールできました。
以上、「最大数に達しました。」エラーが発生した際にお試しください。
- 投稿日:2020-02-19T00:02:37+09:00
【Swift】AirtableとMoyaを使ったAPI通信
はじめに
VUI関係の登壇に伴い、スマートスピーカーとiOSアプリ何かで組み合わせたものを簡単に作りたいと思い、Airtableを見つけました。
アプリに取り入れようと思ったところ、Airtableの使い方に関する記事が少なかったため備忘録も兼ねて記事にしました。・Moya: https://github.com/Moya/Moya (Airtableとの通信用に利用)
・Airtableとは
簡単操作でウェブ上にデータベースを作成して視覚的に操作もできる「Airtable」を使ってみた
何を作るか
自分が大好きなファミレス、デニーズで何曜日にどこで何を食べたかを記録するアプリ
記録したものを参考にスマートスピーカーがメニューをおすすめ(今回の記事では紹介割愛)Airtable準備
今回は初回の準備からTable作成・ドキュメント確認まで説明を記載致します。
Airtable公式サイトで右上のSign upを押下し必要事項を入力し登録を行う
登録後再度サイトを開くとBasesが開かれる。
Workspaceが既にありますが、使い分けるためにWorkspaceを新しく作成したい場合はAdd a workspaceを押下すると新しいWorkspaceが作成できます、(作らなくても可)下記添付画像のような画面になったら Add a base を押下
今回はスマートスピーカー側のAirtableのテストも兼ねており、初期データを楽にいれたかったためcsvを用意していました。
そのため import a spreadsheetを押下
(CSVを用意していない場合は Start from scratch からぽちぽちTable作れます)Choose a .CSV file を押下し、下記のcsvを選択
Dennys.csvtype,name,week,status ご飯,とろ〜り卵とチーズのオムライス,日,普通 肉,大盛りカットステーキ,月,空いてる 肉,トロけるお肉のビーフシチュー,火,空いてる パスタ,熟成卵黄と4種チーズのカルボナーラ,水,普通 ご飯,オマール香るエビドリア,木,普通 デザート,デビルズブラウニーサンデー,金,空いてない デザート,キャラメルハニーパンケーキ,土,空いてないCSVをいれたあとにTable名を変更したり、項目に間違いがないか確認
(私は店名の項目の入れ忘れに気付いてCSV用意したのに結局ここの画面から項目増やしました…)
テーブルの用意ができたらAPIを呼び出すためのドキュメントを確認します。
右上のHELP を押下しAPI documentation押下APIの使用例がとても分かりやすく書いてあります。
Airtableアカウント画面でAPIkeyを作成していない方は、AUTHENTICATIONの項目のaccountを押下しアカウント画面でAPIkeyを作成してください。今回は用途として、記録をするアプリのためCreate recordsを押下しレコードを登録するための情報を確認します。
requestに必要な情報やサンプルresponseもあるため、ここを見るところまできたらアプリに組み込む準備はばっちりです。
Xcode準備
Cocoapodsを利用してMoyaをいれる。
pod 'Moya', '~> 13.0'APIの定義
YOUR_TABLEは自身のTableURL(Create records 押下時右側記載のURL)
YOUR_API_KEYは自身のAPIkeyをいれます。RecordDennysAPI.swiftenum 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とかででいい感じにした方が良いと思いますが…)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実装経験も少なかったため、良い勉強になりました。参考