読者です 読者をやめる 読者になる 読者になる

UIViewに角丸な枠線(破線/点線)を設定する

環境: Swift3

プレイグラウンドで確認。

f:id:xyk:20161128185455p:plain

import UIKit
import PlaygroundSupport

final class DashedBorderAroundView: UIView {
    
    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)
        
        if self.dashedBorderLayer.superlayer == nil {
            self.layer.addSublayer(self.dashedBorderLayer)
            self.layer.cornerRadius = 10
        }
    }
    
    private lazy var dashedBorderLayer: CAShapeLayer = { [unowned self] in
        
        let width = self.bounds.width
        let height = self.bounds.height
        let cornerRadius: CGFloat = 10
        
        let path = UIBezierPath()
        
        path.move(to: CGPoint(x: 0, y: height - cornerRadius))
        path.addLine(to: CGPoint(x: 0, y: cornerRadius))
        path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius),
                    radius: cornerRadius,
                    startAngle: CGFloat(M_PI),
                    endAngle: -CGFloat(M_PI_2),
                    clockwise: true)
        path.addLine(to: CGPoint(x: width - cornerRadius, y: 0))
        path.addArc(withCenter: CGPoint(x: width - cornerRadius, y: cornerRadius),
                    radius: cornerRadius,
                    startAngle: -CGFloat(M_PI_2),
                    endAngle: 0,
                    clockwise: true)
        path.addLine(to: CGPoint(x: width, y: height - cornerRadius))
        path.addArc(withCenter: CGPoint(x: width - cornerRadius, y: height - cornerRadius),
                    radius: cornerRadius,
                    startAngle: 0,
                    endAngle: CGFloat(M_PI_2),
                    clockwise: true)
        path.addLine(to: CGPoint(x: cornerRadius, y: height))
        path.addArc(withCenter: CGPoint(x: cornerRadius, y: height - cornerRadius),
                    radius: cornerRadius,
                    startAngle: CGFloat(M_PI_2),
                    endAngle: CGFloat(M_PI),
                    clockwise: true)
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.blue.cgColor
        shapeLayer.lineWidth = 3
        shapeLayer.lineDashPattern = [3, 6]
        shapeLayer.lineCap = kCALineJoinRound
        shapeLayer.path = path.cgPath
        
        return shapeLayer
    }()
    
}

let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .white

let dView = DashedBorderAroundView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
dView.backgroundColor = .lightGray
dView.center = view.center
view.addSubview(dView)

PlaygroundPage.current.liveView = view

アプリサブミット時の輸出コンプライアンスの確認をスキップする

アプリをiTunes Connectでサブミットするときに以下のように毎回「輸出コンプライアンス」、「広告ID」についての質問に回答する必要がある。
少しの手間だが面倒なのでこの入力をスキップする方法を調べた。

f:id:xyk:20161128175259p:plain

暗号化機能を含まない場合はInfo.plistITSAppUsesNonExemptEncryptionというキーでNOを設定しておくことで「輸出コンプライアンス」の項目が表示されなくなる。

UIWebViewのリクエストにUserAgentを設定する

ios swift

環境: Swift3

UIWebViewのリクエストにUserAgentを設定するには、リクエスト前にUserDefaultsのregisterメソッドでキー名UserAgentで値をセットする必要がある。

UserDefaults.standard.register(defaults: ["UserAgent" : "hoge"])

この時、

// これだと設定されない
UserDefaults.standard.set("hoge", forKey: "UserAgent")

のようにUserDefaultsに普通に保存してもダメでregisterメソッドでデフォルト値として設定すること。

// これを使う
open func register(defaults registrationDictionary: [String : Any])

setメソッドとregisterメソッドの違いは以下参照。
xyk.hatenablog.com


追記

ハマリポイントがあったのでメモ。

UIWebViewで使われるUserAgentを取得する方法は以下。

let ua: String? = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")

取得結果は

Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0 Mobile/14C92 Safari/602.1

となった。
この結果の末尾に独自の文字列(ここでは例としてhoge)を追加して

Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0 Mobile/14C92 Safari/602.1 hoge

これを新たなUserAgentとして更新しようとしたがなぜか更新できなかった。
どうやら使用するUIWebViewのインスタンスに対してwebView.stringByEvaluatingJavaScript(from: "navigator.userAgent")を呼び出すとそれ以降、UserAgentを設定しても更新できないようだ。

なので、以下のようにした。

class WebViewController: UIViewController {

   @IBOutlet weak var webView: UIWebView?

    override func loadView() {
        
        self.updateUserAgent()
        super.loadView()
    }

    func updateUserAgent() {
        guard
            let currentUserAgent = UIWebView().stringByEvaluatingJavaScript(from: "navigator.userAgent"),
            !currentUserAgent.contains("hoge")
        else {
            return
        }
        
        let newUserAgent = "\(currentUserAgent) hoge"
        let dic = ["UserAgent" : newUserAgent]
        UserDefaults.standard.register(defaults: dic)
    }

}

実際に使用するself.webViewには触れず、またself.webViewを使用する前に、新たなUIWebViewインスタンスを作って、そこから元のUserAgentを取り出して更新するようにした。

一度更新すればアプリ内では引き継がれるのでapplication:didFinishLaunchingWithOptionsでやってしまっても良いと思う。

UserDefaultsのregisterDefaultsメソッドについて

ios swift

環境: Swift3

UserDefaultsのregisterDefaultsメソッドについて勘違いしていたのでメモ。

// Swift3 で registerDefaults() から register(defaults: ) に変更になった
open func register(defaults registrationDictionary: [String : Any])

このregisterメソッドを使って登録したDictionary(以降RegistrationDictionaryと呼ぶ)はデフォルト値として使う用でUserDefaultsのデータとしてファイルに書き込まれるわけではない。
あるキーで読み込みした時にそのキーがまだUserDefaultsに存在せず、RegistrationDictionaryに存在すれば、RegistrationDictionaryの値をデフォルト値として返す。
既にキーがUserDefaultsに登録されていた場合、またはその後、そのキーでUserDefaultsに登録された場合は、UserDefaultsの値を返す。

let defaults = UserDefaults.standard

let foo1 = defaults.string(forKey: "Foo")
print("foo1:", foo1) // nil

defaults.register(defaults: ["Foo": "Bar"])

let foo2 = defaults.string(forKey: "Foo")
print("foo2:", foo2) // Optional("Bar")

UserDefaults.standard.set("Hoge", forKey: "Foo")
defaults.synchronize()

let foo3 = defaults.string(forKey: "Foo")
print("foo3:", foo3) // Optional("Hoge")

UserDefaults に保存されているデータをすべて表示する

swift ios

環境:Swift3

UserDefaults に保存されているデータをすべて表示する

for (key, value) in UserDefaults.standard.dictionaryRepresentation().sorted(by: { $0.0 < $1.0 }) {
    print("- \(key) => \(value)")
}

または

if let appDomain = Bundle.main.bundleIdentifier,
    let dic = UserDefaults.standard.persistentDomain(forName: appDomain) {
            
    for (key, value) in dic.sorted(by: { $0.0 < $1.0 }) {
        print("- \(key) => \(value)")
    }
}

キーの辞書順にソートしてから表示している。

Firebaseでプッシュ通知したが既読数が計測されていなかった件

環境: iOS9

Firebaseのプッシュ通知機能を使い、iOS端末に向けてのプッシュ通知をしたところ、送信は問題なくできたのだが、管理画面から確認できる既読数が0にままだったので原因を調べた。

f:id:xyk:20161104132640p:plain

で原因だが以下ドキュメント

Receive Messages in an iOS App  |  Firebase
https://firebase.google.com/docs/cloud-messaging/ios/receive

Handling messages with method swizzling disabledの項にちゃんと書いてあるのだが Method swizzling をオフにした場合にはFIRMessaging.messaging().appDidReceiveMessage(userInfo)を実装する必要があるが、これを実装していなかった。

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    // Let FCM know about the message for analytics etc.
    FIRMessaging.messaging().appDidReceiveMessage(userInfo)
    // handle your message
  }

これを実装したところ、既読数も計測されるようになった。

ちなみにプッシュ通知を受信した時に呼ばれるメソッドは以下記事書いたように2種類あるのだが

xyk.hatenablog.com

fetchCompletionHandler 付きのメソッドの方はアプリの状態によらず必ず呼ばれるのでこちらでappDidReceiveMessage(userInfo)を実装すること。

あと、管理画面上に計測結果が反映されるには、少し時間がかかるので注意。
まず送信数の方が数十分〜数時間後に反映され、それからさらに時間をおいて既読数の方が反映される。

また、iOS10ではプッシュ通知周りの実装が少し変わったので以下を参考に今後修正予定。
(以前の実装のままでもiOS10で動作はする)

quickstart-ios/AppDelegate.swift at master · firebase/quickstart-ios
https://github.com/firebase/quickstart-ios/blob/master/messaging/FCMSwift/AppDelegate.swift

プッシュ通知受信時に呼ばれるメソッドについて

ios swift

環境: Swift2.3

前提となるCapabilitiesの設定

- Push Notifications -> ON  
- Background Modes -> OFF  

プッシュ通知受信時に呼ばれるメソッド

プッシュ通知受信時に呼び出されるメソッドに
1. application:didReceiveRemoteNotification:
2. application:didReceiveRemoteNotification:fetchCompletionHandler:
という似たメソッドが存在するがこの違いについてメモしておく。

// 1
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
    // userInfo の処理

}

// 2
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject],
                 fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    // userInfo の処理

}

基本的には、この2つのメソッドのどちらか片方のみ実装することになる。
両方とも実装していた場合は
2. application:didReceiveRemoteNotification:fetchCompletionHandler:
の方が優先されて呼び出され、
1. application:didReceiveRemoteNotification:
の方は呼び出されないので注意。


1. application:didReceiveRemoteNotification:を実装してプッシュ通知を受信

アプリ未起動時

画面上部に通知表示。
通知をタップすると
application:didFinishLaunchingWithOptions:のみ呼び出される。
application:didReceiveRemoteNotification:は呼ばれない。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

    if let userInfo = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey] {
        // userInfo の処理
    }

}
フォアグラウンド時

通知表示はない。
application:didReceiveRemoteNotification:のみ呼び出される

バックグラウンド時

画面上部に通知表示。
通知をタップするとapplication:didReceiveRemoteNotification:のみ呼び出される


2. application:didReceiveRemoteNotification:fetchCompletionHandler: を実装してプッシュ通知を受信

アプリ未起動時

画面上部に通知表示。
通知をタップすると
application:didFinishLaunchingWithOptions:が呼び出される。
その後
application:didReceiveRemoteNotification:fetchCompletionHandler:が呼び出される。
ここが1とは違い必ず呼び出される。

フォアグラウンド時

通知表示はない。
application:didReceiveRemoteNotification:fetchCompletionHandler:のみ呼び出される

バックグラウンド時

画面上部に通知表示。
通知をタップするとapplication:didReceiveRemoteNotification:fetchCompletionHandler:のみ呼び出される