しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

モバイル版GoogleAnalyticsでFirebaseを選ばない方法

最近はGoogleAnalyticsプロパティー作成時にモバイルアプリを選ぶとFirebase一択になりました。
今日はモバイルアプリでもFirebaseなしのGoogleAnalyticsを使う方法を書いてみます。

f:id:llcc:20170923150323p:plain

まずはウェブサイトとしてプロパティーを作成します。

f:id:llcc:20170923150519p:plain

続けてそのプロパティーを選択してビュー追加画面に移動します。

f:id:llcc:20170923150615p:plain

そこではFirebaseを使わないモバイルアプリを選べるので、選択して作成します。

f:id:llcc:20170923150727p:plain

これでFirebaseなしのGoogleAnalyticsを使う事ができます。

参考URL

support.google.com

iOS11 & Xcode9 で UINavigationController の pushViewController が斜めになる問題対策

push遷移アニメーションが下のように斜めになってしまったので対策を調べてみました。

f:id:llcc:20170922131817g:plain

stackoverflowによると、遷移先のviewDidLoadでtableViewのcontentInsetAdjustmentBehaviorを.neverにすれば良いとのことでした。
popViewControllerの時のアニメーションは遷移元のcontentInsetAdjustmentBehaviorによるので、遷移先・遷移元の両方でセットすると良いかと思います。

if #available(iOS 11.0, *) {
    tableView.contentInsetAdjustmentBehavior = .never
}

1つ1つのtableViewに設定するのが大変な時は、appearanceで設定すれば一括変更されます。

if #available(iOS 11.0, *) {
     UITableView.appearance().contentInsetAdjustmentBehavior = .never
}

それとこちらの現象はUITableViewStyleがgroupedでもplainでも発生するようです。

f:id:llcc:20170922132607g:plain

Xcode9で新規作成したアプリではこの現象は発生しませんでした。
比較したところ、adjustedContentInsetというreadonlyプロパティーの値が違っていました。
今回は調べきれなかったのですが、adjustedContentInsetの値を変更すればcontentInsetAdjustmentBehaviorを使わずに対応できるかもしれません。

print(tableView.adjustedContentInset) // → Xcode9で作成したアプリはtopに64が入ってた、Xcode8以前で作成したアプリのtopは0だった

参考URL

stackoverflow.com

【Swift4】形態素解析をやってみる | NSLinguisticTagger

NSLinguisticTaggerというクラスで形態素解析ができるようなので試してみました。

import UIKit

let text = "I have a beautiful pen."
let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0)

tagger.string = text
tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .tokenType, options: []) { tag, tokenRange, sentenceRange, _ in
    
    let subString = (text as NSString).substring(with: tokenRange)
    print("\(subString) : \(tag?.rawValue ?? "")")
}

文字列を渡した所、下のように分解してくれました。

f:id:llcc:20170921142717p:plain

WhiteSpaceは引数のオプションに.omitWhitespaceを渡すことで除外できます。

let text = "I have a pen."
let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0)

tagger.string = text
tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .lexicalClass, options: [.omitWhitespace]) { tag, tokenRange, sentenceRange, _ in
    
    let subString = (text as NSString).substring(with: tokenRange)
    print("\(subString) : \(tag?.rawValue ?? "")")
}

f:id:llcc:20170921144430p:plain

日本語の分解も可能です。

let text = "関東機械学習協会員"
let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0)

tagger.string = text
tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .tokenType, options: []) { tag, tokenRange, sentenceRange, _ in
    
    let subString = (text as NSString).substring(with: tokenRange)
    print("\(subString) : \(tag?.rawValue ?? "")")
}

漢字が並んでいる場合やひらがなが並んでいる場合もうまく取得できました。

f:id:llcc:20170921143620p:plain

f:id:llcc:20170921143634p:plain

enumerateTagsの引数のscemeにlexicalClassを渡すと品詞分解してくれます。

let text = "I have a beautiful pen."
let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0)

tagger.string = text
tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .lexicalClass, options: []) { tag, tokenRange, sentenceRange, _ in
    
    let subString = (text as NSString).substring(with: tokenRange)
    print("\(subString) : \(tag?.rawValue ?? "")")
}

f:id:llcc:20170921142913p:plain

品詞分解は日本語非対応です。

f:id:llcc:20170921143802p:plain

言語がどのスキーマに対応しているかどうかはavailableTagSchemesを使うと確認できます。

print(NSLinguisticTagger.availableTagSchemes(forLanguage: "en").map { $0.rawValue })
print(NSLinguisticTagger.availableTagSchemes(forLanguage: "ja").map { $0.rawValue })

f:id:llcc:20170921143917p:plain

languageを渡すと言語を判定してくれます。

let text = "I have a beautiful pen."
let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0)

tagger.string = text
tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .language, options: []) { tag, tokenRange, sentenceRange, _ in
    
    let subString = (text as NSString).substring(with: tokenRange)
    print("\(subString) : \(tag?.rawValue ?? "")")
}

f:id:llcc:20170921143148p:plain

日本語、日本語と英語ミックスでも正しく判定できました。

f:id:llcc:20170921143203p:plain

f:id:llcc:20170921143218p:plain

UIViewのdraw(rect:)が呼ばれるタイミングを調べてみる

アプリの高速化のため、UIViewのdraw(rect:)が呼ばれるタイミングを調べてみました。

下のように、普通に画面に貼り付ける時はdraw(rect:)が呼ばれました。

import UIKit

class ViewController: UIViewController {
    let myView = MyView(frame: UIScreen.main.bounds)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(myView)
    }
}

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        print("draw")
    }
}

ちなみに貼付け後、すぐにremoveFromSuperviewをしたらdraw(rect:)は呼ばれませんでした。

view.addSubview(myView)
myView.removeFromSuperview()

そのあと更にaddSubviewを呼んだ場合、draw(rect:)が1回だけ呼ばれます。

view.addSubview(myView)
myView.removeFromSuperview()
view.addSubview(myView)

下のように、タイミングをずらしてaddSubviewを呼んだ場合はdraw(rect:)が2回呼ばれます。

class ViewController: UIViewController {
    let myView = MyView(frame: UIScreen.main.bounds)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(myView)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        myView.removeFromSuperview()
        view.addSubview(myView)
    }
}

frameの値を編集した場合はdraw(rect:)は呼ばれません。

override func viewDidAppear(_ animated: Bool) {
    myView.frame.origin.x = 10
    myView.frame.size.width = 10
    myView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
}

setNeedsDisplayメソッドを呼んだ時もdraw(rect:)が呼ばれます。

myView.setNeedsDisplay()

addSubviewして、直後にsetNeedsDisplayを呼んだ場合は1回だけdraw(rect:)が呼ばれます。

view.addSubview(myView)
myView.setNeedsDisplay()

下のように、親ビューをremoveFromSuperviewしたりaddSubviewした時もdraw(rect:)は呼ばれました。

import UIKit

class ViewController: UIViewController {
    let baseView = UIView(frame: UIScreen.main.bounds)
    let myView = MyView(frame: UIScreen.main.bounds)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        baseView.addSubview(myView)
        view.addSubview(baseView)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        baseView.removeFromSuperview()
        view.addSubview(baseView)
    }
}

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        print("draw")
    }
}

ただ、親ビューのsetNeedsDisplayを呼んだ時はdraw(rect:)は呼ばれませんでした。

class ViewController: UIViewController {
    let baseView = UIView(frame: UIScreen.main.bounds)
    let myView = MyView(frame: UIScreen.main.bounds)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        baseView.addSubview(myView)
        view.addSubview(baseView)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        baseView.setNeedsDisplay()
    }
}

【Swift】Youtube動画をアプリ内で再生する

Google提供のyoutube-ios-player-helperを使って、Youtubeの動画をアプリ内で再生してみました。

github.com

Googleのドキュメントはこちらです。

https://developers.google.com/youtube/v3/guides/ios_youtube_helper

インストール

CocoaPodsでインストールします。

target 'MyApp' do
  use_frameworks!

  pod 'youtube-ios-player-helper'
end

使い方

動画再生はYTPlayerViewクラスを使います。
loadメソッドを呼ぶことで動画の読み込みをします。

import UIKit
import youtube_ios_player_helper

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let youtubeView = YTPlayerView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 240))
        youtubeView.load(withVideoId: "VideoID")
        view.addSubview(youtubeView)
    }
}

アプリを立ち上げると、下のように動画が表示されている事が分かります。

f:id:llcc:20170919125022p:plain

再生ボタンを押すと全画面表示になって再生が始まります。

f:id:llcc:20170919125036p:plain

Youtube上で動画を非公開にすると、下のようにエラーが表示されました。

f:id:llcc:20170919125237p:plain

YTPlayerViewにdelegateをセットする事も可能です。
用意されているメソッドは下の通りです。

extension ViewController: YTPlayerViewDelegate {
    func playerViewDidBecomeReady(_ playerView: YTPlayerView) {}
    func playerView(_ playerView: YTPlayerView, didChangeTo state: YTPlayerState) {}
    func playerView(_ playerView: YTPlayerView, didChangeTo quality: YTPlaybackQuality) {}
    func playerView(_ playerView: YTPlayerView, receivedError error: YTPlayerError) {}
    func playerView(_ playerView: YTPlayerView, didPlayTime playTime: Float) {}
    func playerViewPreferredWebViewBackgroundColor(_ playerView: YTPlayerView) -> UIColor { return .blue }
    func playerViewPreferredInitialLoading(_ playerView: YTPlayerView) -> UIView? { return nil }
}

ロード時、パラメータを渡す事でプレイヤーの制御をする事ができます。
下では[“playsinline”: 1]を渡すことで全画面表示しないようにしています。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let youtubeView = YTPlayerView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 240))
        youtubeView.load(withVideoId: "XXX", playerVars: ["playsinline": 1])
        view.addSubview(youtubeView)
    }
}

利用できるパラメータは下が詳しいです。

YouTube Embedded Players and Player Parameters  |  YouTube IFrame Player API  |  Google Developers

YTPlayerViewは動画の再生メソッド、停止メソッドなども提供しています。
下のようにすることで、動画が自動再生されます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let youtubeView = YTPlayerView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 240))
        youtubeView.load(withVideoId: "XXXX")
        youtubeView.delegate = self
        view.addSubview(youtubeView)
    }
}

extension ViewController: YTPlayerViewDelegate {
    func playerViewDidBecomeReady(_ playerView: YTPlayerView) { playerView.playVideo() }
}

CoreImageのフィルターを試してみる(CICategoryColorAdjustment、その3)

下記事の続きです。
今回もフィルターを使った画像加工を試してみました。

llcc.hatenablog.com

今回はCICategoryColorAdjustmentのCILinearToSRGBToneCurve以降を見ていきます。

Core Image Filter Reference

実装内容は前回同様、以下のようになります。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        let imageView = UIImageView(image: #imageLiteral(resourceName: "image"))
        imageView.frame = CGRect(
            x: (view.frame.width - #imageLiteral(resourceName: "image").size.width) / 2, y: 20,
            width: #imageLiteral(resourceName: "image").size.width, height: #imageLiteral(resourceName: "image").size.height)
        view.addSubview(imageView)
        
        // フィルターをかける
        let filter = CIFilter(name: "CIBoxBlur")
        let ciImage = CIImage(image: #imageLiteral(resourceName: "image"))
        filter?.setValue(ciImage, forKey: kCIInputImageKey)
        if let filteredImage = filter?.outputImage {
            imageView.image = UIImage(ciImage: filteredImage)
        }
    }
}

f:id:llcc:20170919132939p:plain

CILinearToSRGBToneCurve

ドキュメントによると下のようなフィルターのようです。

Maps color intensity from a linear gamma curve to the sRGB color space.

let filter = CIFilter(name: "CILinearToSRGBToneCurve")
let ciImage = CIImage(image: #imageLiteral(resourceName: "image"))
filter?.setValue(ciImage, forKey: kCIInputImageKey)
if let filteredImage = filter?.outputImage {
    imageView.image = UIImage(ciImage: filteredImage)
}

実際に動かしたところ、元画像より少し薄くなりました。

f:id:llcc:20170919132957p:plain

CISRGBToneCurveToLinear

こちらはCILinearToSRGBToneCurveと反対で、SRGBToneCurveをLinearに直すものです。
先程と反対に、色が濃くなりました。

f:id:llcc:20170919140504p:plain

CITemperatureAndTint

色温度と色合いを変更できるフィルターです。
inputNeutralのxの値を少なくすると青っぽく、yの値を大きくすると赤っぽくなりました。
inputTargetNeutralについては効果がわかりませんでした。

let filter = CIFilter(name: "CITemperatureAndTint")
let ciImage = CIImage(image: #imageLiteral(resourceName: "image"))
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(CIVector(x: 6500, y: 0), forKey: "inputNeutral")
filter?.setValue(CIVector(x: 6500, y: 0), forKey: "inputTargetNeutral")
if let filteredImage = filter?.outputImage {
    imageView.image = UIImage(ciImage: filteredImage)
}

f:id:llcc:20170919141313p:plain

CIToneCurve

トーンカーブを変更できるフィルターです。

let filter = CIFilter(name: "CIToneCurve")
let ciImage = CIImage(image: #imageLiteral(resourceName: "image"))
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(CIVector(x: 0.00, y: 0.00), forKey: "inputPoint0")
filter?.setValue(CIVector(x: 0.25, y: 0.25), forKey: "inputPoint1")
filter?.setValue(CIVector(x: 0.50, y: 0.50), forKey: "inputPoint2")
filter?.setValue(CIVector(x: 0.75, y: 0.75), forKey: "inputPoint3")
filter?.setValue(CIVector(x: 1.00, y: 1.00), forKey: "inputPoint4")
if let filteredImage = filter?.outputImage {
    imageView.image = UIImage(ciImage: filteredImage)
}

下のようにx=0.5以降のyを1.0にすることで、白に近い色だけを更に白っぽくしたりできます。

filter?.setValue(CIVector(x: 0.00, y: 0.00), forKey: "inputPoint0")
filter?.setValue(CIVector(x: 0.25, y: 0.25), forKey: "inputPoint1")
filter?.setValue(CIVector(x: 0.50, y: 1.00), forKey: "inputPoint2")
filter?.setValue(CIVector(x: 0.75, y: 1.00), forKey: "inputPoint3")
filter?.setValue(CIVector(x: 1.00, y: 1.00), forKey: "inputPoint4")

f:id:llcc:20170919141910p:plain

CIVibrance

肌の色を保ちつつ、彩度を調整してくれるようです。

let filter = CIFilter(name: "CIVibrance")
let ciImage = CIImage(image: #imageLiteral(resourceName: "image"))
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(1000, forKey: "inputAmount")
if let filteredImage = filter?.outputImage {
    imageView.image = UIImage(ciImage: filteredImage)
}

f:id:llcc:20170919142205p:plain

CIWhitePointAdjust

Adjusts the reference white point for an image and maps all colors in the source using the new reference.

リファレンスによると上のようなフィルターでした。
青色を入れたら濃い目の青に、黄色を入れたらほぼそのままでした。
おそらく画像の基準の色(この写真だと黄色)とinputColorとの差分を元に何かしているのだと思います。

let filter = CIFilter(name: "CIWhitePointAdjust")
let ciImage = CIImage(image: #imageLiteral(resourceName: "image"))
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(CIColor.blue(), forKey: "inputColor")
if let filteredImage = filter?.outputImage {
    imageView.image = UIImage(ciImage: filteredImage)
}

f:id:llcc:20170919142714p:plain

ActionSegueの動作を確認する

ActionSegueの動作を確認してみました。

f:id:llcc:20170919144335p:plain

UINavigationControllerにセットされているController

まずは、下のようにUINavigationControllerにセットされているControllerからのSegueの挙動を確認してみました。

f:id:llcc:20170919144624p:plain

Show

UINavigationControllerのpushメソッドを呼んだ時と同じ挙動になりました。

f:id:llcc:20170919144820p:plain

Show Detail

UINavigationControllerのpushメソッドを呼んだ時と同じ挙動になりました。

Present Modally

UIViewControllerのpresentメソッドを呼んだ時と同じ挙動になりました。

Present As Popover

UIViewControllerのpresentメソッドを呼んだ時と同じ挙動になりました。

UINavigationControllerにセットされていないController

今度はUINavigationControllerにセットされてないControllerの挙動を確認してみました。

f:id:llcc:20170919145034p:plain

Show, Show Detail, Present Modally, Present As Popover

全て、UIViewControllerのpresentメソッドを呼んだ時と同じ挙動になりました。

コード上でUINavigationControllerにセットされたController

コード上でUINavigationControllerにセットされたControllerの挙動も確認しました。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.makeKeyAndVisible()
        if let c = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() {
            
            window?.rootViewController = UINavigationController(rootViewController: c)
        }
        return true
    }
}

Show, Show Detail

UINavigationControllerのpushメソッドを呼んだ時と同じ挙動になりました。

Present Modally, Present As Popover

UIViewControllerのpresentメソッドを呼んだ時と同じ挙動になりました。
Storyboard上でUINavigationControllerにセットした場合と同じ動きをしました。