20191009のSwiftに関する記事は10件です。

AVFoundationを使った動画編集(再生速度を変える)

AVFoundationを使った動画編集(簡単なフェード処理)の続きで、映像の再生速度を変更して早送りにしたりスローモーションにしたりしてみます。

出力用のCompositionとTrackの準備

let srcAsset = AVURLAsset(url: url1)

let composition = AVMutableComposition()

// 出力用VideoTrackを作る
guard
    let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
    debugPrint("Failed to add video tracks")
    return
}

CompositionとTrackの準備については、前回の内容と同じです。

再生速度を指定して映像を追加するメソッドを作る

private func copyVideoTrack(of asset: AVAsset,
                            start: TimeInterval,
                            end: TimeInterval,
                            to track: AVMutableCompositionTrack,
                            duration: TimeInterval,
                            at: TimeInterval) throws {
    let srcVideoTrack = asset.tracks(withMediaType: .video)[0]
    let timeScale = srcVideoTrack.naturalTimeScale

    // 元の映像の切り出す範囲
    let srcTimeRange = CMTimeRange(start: CMTime(seconds: start, preferredTimescale: timeScale), end: CMTime(seconds: end, preferredTimescale: timeScale))

    // 出力先トラックでの挿入位置(再生開始時刻)
    let insertAt = CMTime(seconds: at, preferredTimescale: timeScale)

    // 一旦そのままの長さで挿入する
    try track.insertTimeRange(srcTimeRange, of: srcVideoTrack, at: insertAt)

    // 再生時間(再生速度)を変更する
    track.scaleTimeRange(CMTimeRange(start: insertAt, duration: srcTimeRange.duration), toDuration: CMTime(seconds: duration, preferredTimescale: timeScale))
}

元の映像から、出力用の映像トラックに映像を貼り付けるメソッドを用意します。

元映像の再生速度と貼り付け先の再生速度が変わる関係で若干ややこしいので細かくみていきます。

let srcTimeRange = CMTimeRange(start: CMTime(seconds: start, preferredTimescale: timeScale), end: CMTime(seconds: end, preferredTimescale: timeScale))

まず、元の映像から切り出す範囲を決定します。startendという二つの引数で開始時刻、終了時刻を秒で指定します。timescaleは元映像のtimescaleで統一します。

let insertAt = CMTime(seconds: at, preferredTimescale: timeScale)

次に、出力先トラックの挿入位置を決定します。

try track.insertTimeRange(srcTimeRange, of: srcVideoTrack, at: insertAt)

切り出す範囲と挿入位置が決まったら、一度元のスケール(再生速度)のままトラックに映像を挿入します。

track.scaleTimeRange(CMTimeRange(start: insertAt, duration: srcTimeRange.duration), toDuration: CMTime(seconds: duration, preferredTimescale: timeScale))

その後、scaleTimeRangeメソッドを使って挿入した箇所のスケール(再生速度)を変更します。

最初の引数(CMTimeRange(start: insertAt, duration: srcTimeRange.duration))は、出力用トラックにおける再生範囲です。元映像における再生範囲ではないので注意しましょう。ここでは映像を挿入した位置(insertAt)から元映像における切り出し範囲の長さ(srcTimeRange.duration)分を再生速度変更の対象として指定します。

次の引数(toDuration: CMTime(seconds: duration, preferredTimescale: timeScale))では、対象となる範囲を何秒に圧縮/引き延ばすかを指定します。ここでは、durationという引数で指定した秒数になるように指定しています。もし元映像から再生速度の倍率で指定したい場合はCMTimeMultiplyByFloat64(_:multiplier:)などを使って、元の映像の長さから計算しても良いと思います。

Trackに映像を切り貼りしていく

// それぞれのトラックから最初の5秒を取り出す
do {
    try copyVideoTrack(of: srcAsset, start: 0.0, end: 1.0, to: videoTrack, duration: 1, at: 0.0)
    try copyVideoTrack(of: srcAsset, start: 1.0, end: 14.0, to: videoTrack, duration: 1.0, at: 1.0)
    try copyVideoTrack(of: srcAsset, start: 14.0, end: 15.0, to: videoTrack, duration: 1.0, at: 2.0)
} catch let error {
    debugPrint(error)
    return
}

上で用意したメソッドを使って、元映像から出力用トラックに映像を切り貼りしていきます。上のコードでは、

  1. 元映像の最初の1秒を等倍速で貼り付け
  2. 元映像の1秒〜14秒までの13秒間を1秒間に圧縮して貼り付け
  3. 元映像の14秒〜15秒までの1秒間を等倍速で貼り付け

という形で、再生速度を変えながら元映像の15秒分を3秒に圧縮して追加しています。

Instructionの設定とプレビュー

// 出力用のビデオトラックに対するlayerInstruction(今回は何もしない)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)

// VideoCompositionに対するinstruction
let instruction = AVMutableVideoCompositionInstruction()
instruction.layerInstructions = [layerInstruction]

// instructionはビデオトラック全体に適用されるようにする
instruction.timeRange = videoTrack.timeRange

// VideoComposition
let videoComposition = AVMutableVideoComposition()
videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
videoComposition.renderSize = videoTrack.naturalSize
videoComposition.instructions = [instruction]

let playerItem = AVPlayerItem(asset: composition)
playerItem.videoComposition = videoComposition

let player = AVPlayer(playerItem: playerItem)
playerView.player = player

InstructionやAVPlayerを使ったプレビューについては、AVFoundationを使った動画編集(簡単なフェード処理)と同じなので説明を省略します。

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

[PencilKit]Webブラウザに直接メモを書き込めるアプリを作った

イントロ

6月にあった WWDC 2019 でPencilKitが発表されました。

https://developer.apple.com/documentation/pencilkit/

これは純正メモアプリなどで使用されているペンツールをサードパーティーアプリでも使用できるようにするもので、Apple Pencil関連のアプリ開発のハードルが一気に下がります。

PencilKit 概要

PencilKitの主なパーツについて書いていきます。
間違いがありましたらご指摘いただけると幸いです。

PKCanvasView

Apple Pencilでの書き込みを受け付けるViewです。
UIScrollViewを継承しており、ContentSizeを拡大することでスクロールなども行うことができます。

PKDrawing

Apple Pencilによる書き込みデータです。
Codableを継承しているので、エンコードしてファイルとして保存することもできます。

PKToolPicker

ペンの色を選択したり、消しゴムを選択したりするペンツールセットです。
viewではなくwindowに対して紐付けして使用します。

スクリーンショット 2019-10-09 22.15.07.png

PencilKit周りのいろいろ

var canvasView: PKCanvasView!
var toolPicker : PKToolPicker?

PKCanvasViewの初期化

指での書き込みを無効化

canvasView.allowsFingerDrawing = false

背景の透明化

canvasView.isOpaque = false

PKToolPickerの表示

AppleのDocumentationにあるExampleが大変参考になりました。
https://developer.apple.com/documentation/pencilkit/drawing_with_pencilkit

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    if let window = parent?.view.window, let toolPicker = PKToolPicker.shared(for: window) {
        self.toolPicker = toolPicker
        self.toolPicker!.setVisible(true, forFirstResponder: canvasView)
        self.toolPicker!.addObserver(canvasView)
        self.toolPicker!.addObserver(self)
        canvasView.becomeFirstResponder()
    }
    parent?.view.window?.windowScene?.screenshotService?.delegate = self
}

ツールの表示・非表示の切り替えはsetVisible()で行うことができます。

func togglePickerTool(_ visible : Bool){
    if toolPicker != nil {
        toolPicker!.setVisible(visible, forFirstResponder: canvasView)
    }
}

PKDrawingの保存

PKDrawing単体で管理するのは大変なので、IDなどと一緒に保存します。
保存するデータのひとまとまりを次のクラスで定義します。

public class Note : Codable {
    public var drawing: PKDrawing
    public var uuid = UUID()
    // その他タイトルや最終更新日など必要なものを定義
}

ファイルへの保存

let note = Note(drawing: canvasView.drawing)
let localDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let url = localDirectory.appendingPathComponent("\(note.uuid).custom")

let encoder = PropertyListEncoder()

do {
    let data = try encoder.encode(note)
    try data.write(to: url)
    print("successfully saved item \(note.uuid)")
} catch {
    os_log("Could not save data model: %s", type: .error, error.localizedDescription)
}

ファイルの読み込み

var loadedNote: Note

let decoder = PropertyListDecoder()

do {
    let data = try Data(contentsOf: url)
    loadedNote = try decoder.decode(Note.self, from: data)
} catch {
    os_log("Could not load data model: %s", type: .error, error.localizedDescription)
}

おわりに

今回簡単にApple Pencilを用いたアプリが開発できるようになったので、PencilKitとWKWebViewを組み合わせたアプリケーションを作成しました。

個人的にはQiitaやMedium等の記事に色々思ったことを書き込んでいく、、、ということを想定して作っています。

よろしければぜひ使ってみてください。

Notewind

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

iOS13 復活したpopoverLayoutMarginsをおさらい

概要

前々から使いたかったのに全然思う通りに動いてくれなかったUIPopoverPresentationControllerpopoverLayoutMarginsがiOS13で動くようになっていました!
むしろ動くようになったことでレイアウトが崩れていましたが←
やっと役割を果たしてくれるということで、機能と使用方法のおさらいを簡単にまとめていきます。

popoverLayoutMarginsとは

PopoverのUIViewControllerを設定する際などに使うUIPopoverPresentationControllerの配置に関わるプロパティです。
普通にPopoverを表示するとNavigationBarに被ってしまったりして困る時がありますね。

deactive_margin.png

MarginとあるようにPopoverを表示する際に画面からのマージンを設定することで
NavigationBarに被らないようにできるはずなのですが、、
iOS8以降設定しても表示に全く変化がありませんでした。。

iOS 8 popoverpresentationcontroller popoverlayoutmargin not working - Stack Overflow

これはApple側のバグとしてレポートが提出されていましたが、iOS12になっても全然修正されず(汗
てっきりもう無くなるものだと思っていましたが!!ここへ来てまさかの復活!!笑
iOS13の動作確認中にレイアウトが崩れていたので、半信半疑使ってみて復活に気づきました。
嬉しいような悲しいような?苦笑

使用方法

環境

  • iOS 13.0
  • Xcode 11.0
  • Swift5

コード

と言ってもかなり簡単です。
設定したいマージン値のUIEdgeInsetsをSetするだけです。

Popoverを開きたい親のUIViewControllerか、PopoverであるUIViewControllerのどちらか一方に処理を書けばいいです。
今回は親側に実装してみます。

SampleViewController.swift
import UIKit

class SampleViewController: UIViewController
{
    /// Popoverを開く
    ///
    /// - Parameter sender: UIButton
    @IBAction func openPopover(_ sender: UIButton)
    {
        let popoverVC = PopoverViewController()
        popoverVC.preferredContentSize = CGSize(width: 230, height: 200)
        popoverVC.modalPresentationStyle = .popover
        popoverVC.popoverPresentationController?.sourceView = sender
        popoverVC.popoverPresentationController?.sourceRect = sender.bounds
        popoverVC.popoverPresentationController?.permittedArrowDirections = .left
        popoverVC.popoverPresentationController?.delegate = self
        // CHECK: NavigationBarとButtonに被らないようにMarginを設定
        popoverVC.popoverPresentationController?.popoverLayoutMargins = UIEdgeInsets(top: 50, left: 10, bottom: 0, right: 0)
        present(popoverVC, animated: true, completion: nil)
    }
}

extension ViewController: UIPopoverPresentationControllerDelegate
{
    // MARK: UIPopoverPresentationControllerDelegate

    /// adaptivePresentationStyle(iPhoneで表示させる場合にのみ必要)
    func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
    {
        return .none
    }
}

margin_active.png

これでPopoverが他のパーツに被ることなく表示できますね!

参考

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

SwiftUI コードからステータスバーを白色にする

はじめに

今までは、こちらの記事ステータスバーのスタイル(色)変更方法優先度 まとめにあるように
プロジェクトセッティングのGeneral、もしくはInfoPlist、もしくは、コードからの変更で対応してきましたが、SwiftUIの場合はどうかなと思い調べてみました。

General、もしくはInfoPlistの場合

変わらず今まで通り適用可能。

コードの場合

基本的にSwiftUIではUIViewControllerではなく、structなViewをいじっていますが、
調べたところ、View側ではいじれそうになかった。

UIHostingViewControllerでpreferredStatusBarStyleをoverrideする

class HostingController: UIHostingController<ContentView> {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
}

このようにUIHostingControllerを継承したViewControllerを定義してsceneDelegateでホスティングします。

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let contentView = ContentView()
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = HostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

これで、ステータスバーの色を白に変更できます。

まとめ

各Viewごとにステータスバーを変えたい場合や、NavigationViewを使用している場合に動的にステータスバーを変更したい場合はどうするのかはまだ調べてませんが、SwiftUI側でいじれなさそうな雰囲気。
NavigationBarTitleの色変更もappearanceで変更しないといけなかったりで少し使いづらい印象があります。

何か上手いやり方があればアドバイスしていただきたいです。

お読みいただきありがとうございました。

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

structのMemberwise Initilaizer(swiftが用意してくれてるinit)の挙動

structの生成ができない!と焦った経験があり、忘れないようにメモ程度で残します。

structのMemberwise Initilaizerについて

struct MyData {
    var name: String
}

上記のようなstructはinitを定義してなくても

let data = MyData(name: "hoge")

とMemberwise Initilaizerが存在します。

ただ場合によっては利用出来ないこともあります。

struct MyData {
    var name: String
    private var age: Int?
}

上記のようなstructがある場合、MyDataを生成することは不可能です。

ageがprivateになっているため、initを用意してくれてしまうとprivateとしての意味がなくなってしまうから。

だと思います。

struct MyData {
    var name: String
    private var age: Int?

    init(name: String, age: Int?) {
        self.name = name
        self.age = age
    }
}

そういうときにはinitを定義してあげるしかないですね。

privateな変数がある場合、initを定義せずにデータを生成する方法

privateな変数があるけどinitがないstructはたくさんのプロジェクトで使われてるはずです。

生成できないけど、どこで使われるのか。

import Foundation

struct MyData: Codable {
    var name: String
    private var age: Int?
}

jsonデータをdecodeするために、Codableを採用してる場合です。


ちなみにprivateな変数があるstructを生成しようとすると

error: 'MyData' initializer is inaccessible due to 'private' protection level

と怒られますが、
あまり経験したことがなく、
initも特に深く考えずに使ってることが多かったので焦りましたね。

initしてるだけだから、特にprivateの変数に外部からなにかしようとしてないんだけど!!なんで!?と思いハマっちゃいましたw

用意されるinitはinternalのものになるので、privateを勝手にinternalには変えてくれないということです。すごい当たり前の挙動ですねw

まあ考えてみれば当たり前のことなので、
お恥ずかしいですが、自分のような人はあまりいないと思いますが、メモとして残します!

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

NSButtonをCombineで扱う

NSButtonをCombineで扱いたい。

NSTextFieldNSTextViewはそれぞれNotificationCenterでNSControl.textDidChangeNotification/NSText.didChangeNotificationを利用すればよいが、NSButtonにはそういう感じのNotificationはない。

じゃあどうするかというと、KVO (Key-Value Observing)を使う。
CombineではNSObject#publisher(for:options:)を使うことで、従来のKVOをCombineの世界に持ってこれる1

しかし、素直に

// button is NSButton

button.publisher(for: \.state, options: [.initial, .new])

と書いても、initialを除いて変更が流れてこない。

なぜなら、NSButton.stateの実態はNSButtonCellへのproxyなため 2

なので、button.cell.stateを見るようにするとよい。

// button is NSButton

button.cell!.publisher(for: \.state, options: [.initial, .new])

  1. しかし、ドキュメントには developer.apple.com/documentation にも Xcode 内のものにも載っていない。Xcode上の補完では出るし説明も出るが、Jump to Definition しても Foundation framework の定義に飛ぶだけ(そしてその定義の中にも見当らない)、という謎がある。 

  2. 最初に見つけたのは https://stackoverflow.com/a/3223395 。ドキュメントをよくよく見ると

    NSButton and NSMatrix both provide a control view, which displays an NSButtonCell object. However, while a matrix requires you to access the button cell objects directly, most button class methods act as “covers” for identically declared button cell methods. In other words, the implementation of the button method invokes the corresponding button cell method for you, allowing you to be unconcerned with the existence of the button cell. The only button cell methods that don’t have covers relate to the font used to display the key equivalent and to specific methods for highlighting or showing the state of the button.
    (from https://developer.apple.com/documentation/appkit/nsbutton)
    と書いてあるのでまあわからなくもないが、KVOのことには一切触れられていないので、なかなか厳しい。まあそもそもドキュメントにはNSButtonCellにKVOができるとも書いてないけど。 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI 破線のボタンの作り方

破線のボタンの作り方

備忘録です。

hasen_sample_1.png

StrokeStyleのdashの数値で破線の長さを変更できます。

var body: some View {
        VStack(alignment: .center) {
            Button(action: {}) {
                HStack(spacing: 4) {
                    Image(systemName: "plus.circle")
                    Text("HogeHoge")
                }
                .foregroundColor(.blue)
                .padding(12)
                .overlay(
                    RoundedRectangle(cornerRadius: 8)
                        .strokeBorder(
                            style: StrokeStyle(
                                lineWidth: 2,
                                dash: [4]
                            )
                    )
                        .foregroundColor(.blue)
                )
            }

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

Swift Firebaseを使って画像をuploadする

Firebaseで画像をuploadする概要

ざっくりしか書いてありません、概要でも掴んでもらえたらと書きました

// 1:画像をデータに変換
guard let imageData = image.jpegData(compressionQuality: 0.3)

compressionQualityは0.0~1.0まで設定できます

// 2:ストレージイメージ参照を作成->保存するFirestorageの場所
let imageRef = Storage.storage().reference().child("/productImages\/(name).jpg")

productImagesは今回はfirebase上のstorageにフォルダを作成しています
後述のnameに関してはOutletのテキスト入力を想定しています
2箇所(productImages, name)は任意に変更可能です

// 3:メタデータを設定
let metaData = StorageMetaData()
metaData.contentType = "image/jpeg"

"image/jpeg"はstorage上のタイプを指定しています

// 4:データをアップロード
imageRef.putData(imageData, metadata: metaData) { (metaData, error) in
    if let error = error {
        self.handleError(error: error, msg: "Unable to upload image.")
    return
   }

// 5:画像がアップロードされたら、ダウンロードURLを取得
imageRef.downloadURL { (url, error) in
    if let error = error {
         self.handleError(error: error, msg: "Unable to download URL.")
    return
    }
    guard let url = url else { return }

// 6:新しいドキュメントをFirestoreコレクションにアップロード
// uploadDocumentは関数を用意してあげる
// Firebaseへの登録時の構造体を初期化して、setDataメソッドでデータをセットしてあげる
    self.uploadDocument(url: url.absoluteString)
     }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NSDateComponentsにinit?を追加するとクラッシュするパターン

対象は NSDateComponents です。
DateComponents ではないためこの問題に引っかかる人は少ないと思いますが、簡単に情報を残しておきます。

Failable Initializer(init?)を追加して return nil するとクラッシュすることがあります。
環境は Xcode11.1 (11A1027) です。

以下がクラッシュするコードです。
(こんなメソッドいらないだろう? みたいなツッコミはなしで?)

extension NSDateComponents {
    @objc convenience init?(string: String) {
        guard ! string.isEmpty else {
            return nil
        }
        self.init()
        self.hour = Int(string)!
    }
}

Xcode10のときは動いていました。
Xcode11だとこれを呼び出したあとクラッシュします。
以下のように変更すると動きます。

extension NSDateComponents {
    @objc convenience init?(string: String) {
        self.init() // `return nil` する場合も必ず呼び出す。
        guard ! string.isEmpty else {
            return nil
        }
        self.hour = Int(string)!
    }
}

NSObject を適当に継承したクラスで super.init() を呼び出さなくても問題はありませんでした。
NSDateComponents(もしかしたら他のクラスでも)の実装の問題なのでしょうか。

自分のコードのお行儀が悪いのかバグなのかはわかりませんでしたがお気をつけください。

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

[SwiftUI]UIHostingControllerの最小サイズを取得する

SwiftUIをUIKitの世界に連れてくるにはUIHostingControllerを利用し、UIKitのコンポーネントとしてコンテナ化します。
モーダルなどとして利用する場合は、UIHostingControllerをそのまま使えば良いですがコンテナビューとして使いたい時は最小サイズが知りたくなります。
sizeThatFitsのメソッドが生えているので、そこに対してInt.maxのサイズを渡してあげれば最小をとることができます。

let hostingController = UIHostingController(rootView: content())
let size = hostingController.sizeThatFits(in: CGSize(width: Int.max, height: Int.max))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む