20201123のiOSに関する記事は12件です。

【SwiftUI】Likeボタンとリスト内セルにボタンを実装する場合の注意点

はじめに

SwiftUIでFirebaseからデータを読み取り、これまで作成したカード内容を更新することを目的とする。

今回はカード内のLikeボタンの実装を行います。

結論を最初に言うと、ListView内でButtonを実装する場合はそれぞれのImageに.onTapGestureを設定する必要があります。
その点がわかっていなかったため、実装に時間がかかってしまいました。

前回までの記事は以下を参考ください。

参考記事
【SwiftUI】Firebaseからデータを読み取り、ListViewのRowを更新する

開発環境

OSX 10.15.7 (Catalina)
Xcode 12.2.0
CocoaPods 1.10.0

Like機能の実装

ボタンの設定に時間がかかってしまったため、FirebaseのLike数の更新機能の実装までは至りませんでした。
今回はタップした際にハートの色を変更する機能のみ実装します。
設定のみしたaction機能は次回以降に実装します。

実装方法

struct ContentView: View {

    @State private var isLiked = false

    var body: some View {
        VStack {
            Text(isLiked ? "Liked!" : "unLiked!")
            HeartButton(isLiked: $isLiked)
        }
    }
}

struct HeartButton: View {
    @Binding var isLiked : Bool


    var body: some View {
        Button(action: {
            self.isLiked.toggle()
        }, label: {
            Image(systemName: isLiked ? "heart.fill": "heart")
                .font(.largeTitle)
                .foregroundColor(Color.red)
                .frame(width: 100, height: 100)
        })
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

タップ前
ContentView_swift_—_Edited.png

タップ後
ContentView_swift_—_Edited.png

コード全体

今回機能の実装にあたり、View内のコードを見直し、修正しました。
上記のコードを参考に、List内のカードに実装します。
List内にButtonを実装する場合は、実装した場合はセル全体が選択可能になってしまい、特定のボタンのみを選択することができません。
そのため、ボタンの代わりにtextやimageに.onTapGestureを実装する必要があります。
現在は色が変わるだけしか実装できていません。
今後はタップした場合にFirebaseを更新し、Like数が増減できるようにする予定です。

struct ContentRowView: View {
    var id  = ""
    var user = ""
    var userImage = ""
    var haiku = ""
    var mapImage = ""
    var place = ""
    var likes = ""
    // ○秒、○日、○年前を表示

    var createdDate = ""

    static let formatter = RelativeDateTimeFormatter()

    // 以下を追記
    @State var isLiked = false

    var body: some View {

        ContentRowView.formatter.locale = Locale(identifier: "ja_JP")

        let fmt = ISO8601DateFormatter()
        let date1 = fmt.date(from: createdDate)!
        let components = Calendar.current.dateComponents(
            [.day, .year, .month, .minute, .second],
            from: Date(),
            to: date1
        )
        let timeAgo = ContentRowView.formatter.localizedString(from: components)

        return VStack {
            // mapImage
            VStack {
                AnimatedImage(url: URL(string: mapImage)!)
                    .resizable()
                    //.aspectRatio(contentMode: .fit)
                    .cornerRadius(12.0, antialiased: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
                HStack {
                    Spacer()
                    Text(place).font(.caption).foregroundColor(.gray)
                        .padding(.bottom, 5)
                }
            }
            // Haiku
            Text(haiku).font(.title3).fontWeight(.bold)
            // Likes Number
            HStack {
                Image(systemName: "heart").font(.headline).foregroundColor(Color("pinkColor"))
                Text(likes).font(.headline).foregroundColor(Color("pinkColor"))
                Spacer()
            }.padding(.top, 5)
            // Info
            HStack {
                AnimatedImage(url: URL(string: userImage)!)
                    .resizable()
                    .frame(width: 30, height: 30)
                    .clipShape(Circle())

                Text(user).font(.headline).fontWeight(.light)
                + Text("・").font(.headline).fontWeight(.light)
                + Text(timeAgo).font(.headline).fontWeight(.light)
                Spacer()
                    // If Comment Button was push, it call CommentView.
                Image(systemName: "text.bubble")
                    .font(.title)
                    .foregroundColor(.gray)
                    .onTapGesture {
                        print("Button Tapped")
                    }
                    .frame(width: 30, height: 30)

                // Like Button push Image to add like's Number.
                Image(systemName: isLiked ? "heart.fill": "heart")
                    .font(.title)
                    .foregroundColor(Color("pinkColor"))
                    .onTapGesture {
                        self.isLiked.toggle()
                    }
                    .frame(width: 30, height: 30)
            }.padding(5)
        }.padding(.top, 8).frame(height: 391)
    }
}

追記

エラー
下記のエラーを確認して、修正しようとしましたが、現状では解決できないエラーのようです。
無視しても致命的にはならない現象のため、いったんそのままにします。

nw_protocol_get_quic_image_block_invoke dlopen libquic failed

参考文献
Strange error nw_protocol_get_quic_image_block_invoke dlopen libquic failed
Swift Firebase Connection

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

Flutterチュートリアルを咀嚼する part2 -routeとnavigator-

part1ではFlutterの概要を掴んだ
part2ではpart1で作ったstartup_namerアプリにスマホアプリによくある機能を搭載する
・アイコンの利用
・いいね機能
・別ページへの遷移

対象のチュートリアル

https://codelabs.developers.google.com/codelabs/first-flutter-app-pt2/#0

コピペしてできあがったもの

気に入った名前にいいねができるようになった
スクリーンショット 2020-11-22 0.01.52.png

右上の(名前なんて言うのこれ)を押すと右にスライドしてもう一つのページが表示されるようになった
(いいねしたものだけが表示される)
スクリーンショット 2020-11-22 0.02.48.png

ソースコード

main.dart
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      theme: ThemeData(
        primaryColor: Colors.white,
      ),
      home: RandomWords(),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  _RandomWordsState createState() => _RandomWordsState();
}

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _saved = Set<WordPair>();
  final _biggerFont = TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
        actions: [
          IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggestions(),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
        padding: EdgeInsets.all(16.0),
        itemBuilder: /*1*/ (context, i) {
          if (i.isOdd) return Divider(); /*2*/

          final index = i ~/ 2; /*3*/
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10)); /*4*/
          }
          return _buildRow(_suggestions[index]);
        });
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _saved.remove(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }

  void _pushSaved() {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (BuildContext context) {
          final tiles = _saved.map(
            (WordPair pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();

          return Scaffold(
            appBar: AppBar(
              title: Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ),
    );
  }
}

順々にみていく

ハート形アイコンの追加

ハートアイコンと必要な変数の設定

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _saved = Set<WordPair>();     // NEW
  final _biggerFont = TextStyle(fontSize: 18.0);
  ...
}
Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair); //NEW
  return ListTile(
    title: Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: Icon(   // NEW from here... 
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),                // ... to here.
  );
}

Set

https://api.flutter.dev/flutter/dart-core/Set-class.html
_savedはいいねをした文字をメモリ上に保管するための変数

contains()

https://api.flutter.dev/flutter/package-collection_collection/PriorityQueue/contains.html
対象が_savedに格納されていたらtrue、そうでない場合はfalseを返す
true/falseはalreadySavedに格納される

ListTile.trailing

https://api.flutter.dev/flutter/material/ListTile/trailing.html
ListTile(リストの1行分のデータ)のタイトルの後に設定するIcon用のプロパティ

Icon

https://api.flutter.dev/flutter/widgets/Icon-class.html

Icons.favorite

https://api.flutter.dev/flutter/material/Icons/favorite-constant.html
色付きのハートアイコン
色はcolor:~のところで赤色としている
alreadySavedがtrueならこのアイコンを赤色に設定している

Icons.favorite_border

https://api.flutter.dev/flutter/material/Icons/favorite_border-constant.html
色なし枠だけのハートアイコン
alreadySavedがfalseならこちらを設定している

Colors

https://api.flutter.dev/flutter/material/Colors-class.html
色を司るクラス

Colors.red

https://api.flutter.dev/flutter/material/Colors/red-constant.html
赤色を引き出せる

いいね

タップしたらその単語が(メモリ上に)保存され、ハートアイコンが赤色になる

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return ListTile(
    title: Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: Icon(
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),
    onTap: () {      // NEW lines from here...
      setState(() {
        if (alreadySaved) {
          _saved.remove(pair);
        } else { 
          _saved.add(pair); 
        } 
      });
    },               // ... to here.
  );
}

onTap

https://api.flutter.dev/flutter/cupertino/CupertinoTabBar/onTap.html
アイテムがタップされたときに呼び出されるコールバック。

setState()

https://api.flutter.dev/flutter/material/ScaffoldFeatureController/setState.html
Flutterにstate(状態)が変わったことを知らせる
これによりStatefulWidgetの状態が変化する
今回はalreadySavedがtrueなら、_savedから対象を取り除き(いいねをやめる)、falseなら追加する

右ページへの移動

class _RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
        actions: [
          IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}

IconButton

https://api.flutter.dev/flutter/material/IconButton-class.html
押したら(onPressed)反応するアイコンボタン
今回は_pushSavedを呼び出している

Icons.list

https://api.flutter.dev/flutter/material/Icons/list-constant.html
リスト形のアイコン(画面右上にでているの)

  void _pushSaved() {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (BuildContext context) {
          final tiles = _saved.map(
            (WordPair pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();

          return Scaffold(
            appBar: AppBar(
              title: Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ),
    );
  }
}

Navigator

https://api.flutter.dev/flutter/dart-html/Navigator-class.html
Flutterでは新しいページのことをrouteと呼ぶ。
根っこ(root)ではなく経路とかの意味のルートである。
Navigatorは別のrouteへユーザを導く。
※使い方

  Navigator.of(context).push(
  );

MaterialPageRoute

https://api.flutter.dev/flutter/material/MaterialPageRoute-class.html
これが右に移動したら現れる新しいページの本体
中身はいいねした単語がリスト表示されている

全体のテーマを変える

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      theme: ThemeData(          // Add the 3 lines from here... 
        primaryColor: Colors.white,
      ),                         // ... to here.
      home: RandomWords(),
    );
  }
}

ThemeData

https://api.flutter.dev/flutter/material/ThemeData-class.html
MaterialAppのthemeを変更することでアプリ全体のテーマを変更できる
今回は白くした

これでWidgetの使い方
アイコン、新しいページの生成方法が学べた
基礎を理解できる良いチュートリアルだと思う

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

[SwiftUI] Viewを強制再読み込みする賢くない方法

どうしてもViewを再読み込みしたいが、どうやってもうまくいかない、という場合、賢くないですがこの方法でいけます。
SwiftUIのお気持ちに沿って作っていればそもそも強制再読み込みは必要ないはずなので、対処法として正しいのは設計の見直しです。

対処

struct HogeView: View {
    var body: some View {
        Hoge()
    }
}

を強制再読み込みしたい場合、

struct HogeView: View {
    @State private var flag = true

    func refresh(){
        flag.toggle()
    }

    var mainView: some View {
        Hoge()
    }

    var body: some View {
        Group{
            if flag{
                mainView
            }else{
                mainView
            }
        }
    }
}

としてあげれば、flagを切り替えるたびにmainViewが計算しなおされるので実質再読み込みになります。

自戒

本当に賢くないのでSwiftUIをちゃんと勉強します。

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

SwiftUIだけど画面遷移はUIKitでやる

画面遷移処理を各画面から切り離したり、カスタムURLスキームなどを使って任意の画面に遷移できるようにする処理をSwiftUIでもやりたい。でもSwiftUIだけで実現する方法がわからない。。。
難しそうなところは今後のSwiftUIの進化に期待するということで、画面遷移は無理せずUIKitベースでやってしまえば良さそうだなと思い始めました。

環境

  • Xcode 12.0

実装

各画面のレイアウトはSwiftUIでさくっと作ってしまって画面遷移に関連する部分はUIKitベースで処理するために、遷移先はUIHostingControllerを使う。ViewControllerを見つける処理は従来通り。

var window: UIWindow? {
    guard let window = (UIApplication.shared.connectedScenes.first?.delegate as? UIWindowSceneDelegate)?.window else { return nil }
    return window
}

/// 全面に表示されているViewControllerを見つける
func topViewController(_ vc: UIViewController? = nil) -> UIViewController? {
    guard let vc = vc ?? window?.rootViewController else { return nil }
    if let presented = vc.presentedViewController {
        return topViewController(presented)
    }
    return vc
}

/// NavigationControllerを見つける
func navigationController(_ vc: UIViewController) -> UINavigationController? {
    if let result = vc as? UINavigationController {
        return result
    }
    for child in vc.children {
        if let result = navigationController(child) {
            return result
        }
    }
    return nil
}

@main
struct MainApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL(perform: { _ in
                    guard let topVC = topViewController() else { return }
                    if let navVC = navigationController(topVC) {
                        navVC.show(UIHostingController(rootView: TestView()), sender: nil)
                    } else {
                        topVC.present(UIHostingController(rootView: NavigationView(content: { TestView() })),
                                      animated: true,
                                      completion: nil)
                    }
                })
        }
    }
}

SwiftUIで作った画面をXcodeのDebug View Hierarchyで見てみると、ViewControllerらしきコンポーネントがたくさん使われているようだったので、上記の実装は「NavigationViewを使えばUINavigationControllerが内部的には使われているかもしれない」とか、「sheetでモーダル表示したらpresentedViewControllerで遷移先のViewControllerを見つけられるかもしれない」という思い込みで実装してみました。
SwiftUIのNavigationViewを使っている場合にUINavigationControllerを探索可能かどうか不明でしたが、UINavigationControllerを継承していそうなクラスが使われているようでした。

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

AVPlayerViewControllerのコントロールバーを監視する

概要

画面をタップすると表示・非表示が切り替わるコントロールバーを監視します。
あまりニーズがなさそうな情報ですが、つい最近コントロールバーの表示に合わせて自作UIを表示するという要件が実際にありましたのでメモを兼ねて投稿します。

開発環境

Xcode 12.1
Swift 5

AVPlayerViewControllerのレイヤー構成

AVPlayerViewControllerの動画再生時の画面レイヤーはこのようになっています。

目的のバーはAVViewの配下にあります。

監視対象

バーの表示・非表示の切り替えはAVViewのisHiddenプロパティではなく親のAVTouchIgnoringViewのisHiddenプロパティで行われているのでこいつを監視します。
※Objectice-Cの場合は"isHidden"ではなく"hidden"キーになるようです

実装

ViewController.swift
    @IBAction func pressedMoviePlayButton() {
        let playerViewController = CustomAVPlayerViewController()
        self.present(playerViewController, animated: true) {
            playerViewController.player?.play()
            playerViewController.find(view: playerViewController.view)
        }
    }
CustomAVPlayerViewController
    private var observers = [NSKeyValueObservation]()

    override func viewDidLoad() {
        super.viewDidLoad()

        let path = Bundle.main.path(forResource: "sample", ofType: "mp4")!
        let url: URL = .init(fileURLWithPath: path)
        let item: AVPlayerItem = .init(url: url)
        let player: AVPlayer = .init(playerItem: item)

        self.player = player
    }

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

        // 監視を解除する
        observers.forEach {
            $0.invalidate()
        }
        observers.removeAll()
    }

    // AVTouchIgnoringViewを探す
    func find(view: UIView) {

        let targetViewName = "AVTouchIgnoringView"

        view.subviews.forEach {

            if !self.observers.isEmpty { return }

            // isHiddenプロパティをobserveする
            if String(describing: type(of: $0)).isEqual(targetViewName) {               
                self.observers.append($0.observe(\.isHidden, options: .new, changeHandler: { (_, change) in
                    print("\(change.newValue)")
                }))
                return
            }
            self.find(view: $0)
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIの多言語化

概要

LocalizedStringKeyを使ってSwiftUIの多言語化を行います。

3行まとめ

  • Textに文字列定数を渡すと、文字列定数をキーにしてローカライズが行われます。XLIFFのエクスポートもサポートされます。
  • LocalizedStringKeyを使うと、引数をキーにして各種コンポーネントのローカライズ対応ができます。が、XLIFFのエクスポートはサポートされません。
    • NSLocalizedString をコメントの形としてつけておくとXLIFFのエクスポートに対応することが可能です。

Textの多言語化

Textのパラメータに、ダブルクォーテーションで囲われた文字列定数を設定します。
Export for localizationを行うと、Textの文字列を定数にしたものが全てXLIFFに出力されます。これをもって翻訳に使ったり、SwiftUIのプレビューから各言語の表示を確認できたりできます。

コード

SwiftUI

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environment(\.locale, Locale(identifier: "ja"))
    }
}

XLIFF

<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
  <file original="l18/en.lproj/InfoPlist.strings" datatype="plaintext" source-language="en" target-language="ja">
    <header>
      <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="12.2" build-num="12B45b"/>
    </header>
    <body>
      <trans-unit id="CFBundleName" xml:space="preserve">
        <source>l18</source>
        <note>Bundle name</note>
      </trans-unit>
    </body>
  </file>
  <file original="l18/en.lproj/Localizable.strings" datatype="plaintext" source-language="en" target-language="ja">
    <header>
      <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="12.2" build-num="12B45b"/>
    </header>
    <body>
      <trans-unit id="Hello, world!" xml:space="preserve">
        <source>Hello, world!</source>
        <note>No comment provided by engineer.</note>
      </trans-unit>
    </body>
  </file>
</xliff>

Textに定数を設定する場合以外

例えばLabelを使う場合などは、文字列の定数を設定したとしてもその定数をソースとしたXLIFFは出力されません。この場合、従来のコードで使っていたNSLocalizedString を使って対応することになると思います。

Labelのコード

SwiftUI

import SwiftUI

struct ContentView: View {
    let text = NSLocalizedString("Hello, world!", comment: "Hello, world!")
    var body: some View {
        Label(text, systemImage: "arrow.uturn.up")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environment(\.locale, Locale(identifier: "ja"))
    }
}

SwiftUIで言語のプレビューが効かない問題と解消法

しかし、NSLocalizedStringで対応を行なった場合、SwiftUIのプレビューでlocaleのenvironmentを切り替えたとしても言語の確認ができないという問題が発生します。
そこでLocalizedStringKeyを使います。

LocalizedStringKeyにキーを指定すると、SwiftUIのプレビューで確認ができるようになります。
ただし、LocalizedStringKeyもXLIFFのエクスポートに対応していないので、コメントの形としてNSLocalizedStringを書く形にするとXLIFFへのエクスポートにも対応できます。

SwiftUI

import SwiftUI

struct ContentView: View {
    // NSLocalizedString("Hello, world!", comment: "Hello, world!")
    let text = LocalizedStringKey("Hello, world!")
    var body: some View {
        Label(text, systemImage: "arrow.uturn.up")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environment(\.locale, Locale(identifier: "ja"))
    }
}

追記

LocalizedStringKeyのキーは、ローカライズされていない時にデフォルトで表示されるテキストになるので、多くの場合は表示される英語のテキストをキー名にしたほうがいいんじゃないかと思います。

OK

LocalizedStringKey("Hello, world!")

NG

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

XcodeでiMessage Extensionを削除する方法

XcodeでiMessage Extensionを追加するときは、「File」 > 「New」 > 「Target...」から簡単に行えますが、、
削除の手順が分からなかったため、Appleのテクニカルサポート(DTS)へ問い合わせを行なった記録です。

削除手順

以下の3手順にしたがって削除していきます。

1. Xcodeでターゲットを削除する

ターゲットを削除するには、プロジェクトツリーからプロジェクトを選択します。

D198AE5F-A429-4455-ACDF-0D06E79A5534.png
※ すみません、背面にいたQiitaの緑色が写り込んでしまいました、、 ?

そのあと、ターゲット(iMessage Extension)を選択し、右クリックで削除するのですが、、
「General」の左側にいるアイコンを押すと、こちらのサイドバーが現れます。(はじめ、これに気づくのに苦労しました...)

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_141810_1e847253-8c09-6cc6-b859-7d4d35da6538.png

「Delete」でターゲットは無事に削除されます。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_141810_c54a1996-5a8a-f1d8-93f0-90e50bba9891.png

2. 「Manage Schemes...」をクリックしてスキームを削除する

スキームの削除は、トップメニューから行えます。
「Product」 > 「Scheme」 > 「Manage Schemes...」 から管理画面を表示します。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_141810_3e86fe48-35c0-9a72-4037-23c39eda6adc.png

リストの中から該当するもの(今回は iMessage Extension)を選択し、左下のマイナスボタンをクリックします。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_141810_d8c33a2b-57cb-a3b4-5251-e287002e5bb9.png

「Delete」でスキームが無事に削除されます。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_141810_645d1e72-d925-91c6-9e19-1bce2142f5e9.png

3. 関連するソースコードを削除する

最後に関連するソースコードをプロジェクトツリーから右クリックで削除します。
私の場合は、これだけでした。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_141810_dd98ccda-a873-da54-2b8a-cfe3e3614a66.png

おわりに

Appleへの問い合わせは、2018年9月時点のものです。 ※ キャプチャは Xcode 11.1
操作方法などに変更がある場合もありますので、ご注意ください。

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

[iOS]FDL(Firebase Dynamic Links)とは

Firebase Dynamic Linksとは

ユーザーがあるURLを踏んだ場合、

  • iOSデバイスを使っている時、
    • あるアプリがインストールされている時は、そのアプリでコンテンツを開く
    • インストールされていない時は、そのアプリのストアのページを開く
  • Androidデバイスを使っている時、
    • あるアプリがインストールされている時は、そのアプリでコンテンツを開く
    • インストールされていない時は、そのアプリのストアのページを開く
  • そのほかのデバイスを使っている時は、Webブラウザでコンテンツを開く

という風に、デバイスやアプリのインストール状況に応じて違う振る舞いをしたい時がある。

ただ、しかし、それぞれの振る舞いごとに違うURLを用意していては、何個もURLが必要になりユーザーがどれをタップしていいかわからないし、そもそもURLを何個も書ききれないだろう(例えば、ツイッターの一つの投稿にURLを5つも載せるのは無理である)。

一つのURLだけで、状況に応じて違う振る舞いができるのが、Firebase Dynamic Linksである。

下記ページのように、FDLの動きのフローチャートを見ると分かりやすい。
Dynamic Links をデバッグする

導入手順

Firebase Dynamic Linksの生成

iOS で Dynamic Links を作成する

Firebase Dynamic Linksをアプリ側で受け取る

iOS でダイナミック リンクを受信する

なおリンク先に記載があるが、iOS9以降ではカスタムURLスキームと Universal Linksが両方使われる(アプリインストール直後のFDLのみ、カスタムURLスキームとして受け取る)事に注意。

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

Auto LayoutのStack Viewの便利さ

はじめに

Auto Layoutを勉強していてStack Viewの便利さを知ったので記事を書こうと思います。
また、Auto Layoutはエンジニア必須スキルで、ちゃんと理解できれば他のエンジニアと差をつけられる要素になるみたいです。
しっかり使いこなせるようになりたいですね。

Stack Viewとは何か

公式ドキュメントにはこのように書かれています。

Stack Viewは、複雑な制約を導入することなく、自動レイアウトの機能を活用する簡単な方法を提供します。

要するに、本来なら複雑な制約を書いて実装するけど、Stack Viewを使えば簡単に実装できるようになります!って感じです。

使ってみた

今回はボタンを並べて、後からボタンを追加する作業にStack Viewを使っていきます。

Stack ViewをViewに追加

HorizontalとVerticalがあります。
Horizontalは水平なので横並び、Verticalは垂直なので縦並びで設定したい時に使います。
今回はHorizontalを使います。
スクリーンショット 2020-11-23 14.39.55.png

Stack Viewにボタンを上から載せて追加

3つのボタンを並べます。
スクリーンショット 2020-11-23 14.59.25.png

配置を設定

topとcenterの制約を設定します。
右下にあるAdd New Constraintsでtopの制約を追加します。
スクリーンショット 2020-11-23 15.01.57.png
次に、Stack Viewをコントロール押しながらドラックしてViewで離します。
すると画像のような選択肢が出てくるので、Center Horizontallyを選択肢します。
スクリーンショット 2020-11-23 15.03.18.png
これで配置が完了しました。
それぞれのボタンのスペースを8に設定してみました。
スクリーンショット 2020-11-23 15.09.58.png

ボタンを追加する

さて、ここでボタン追加の変更が入ったとします。Stack Viewでなければボタンを追加して、配置なども1からやり直しでしたが。
Stack Viewを使っているので、Stack Viewのなかにボタンをドロップするだけで配置することができます。
もちろん、さっき設定したスペースの8も自動で設定されますので、設定し直す必要がありません。
スクリーンショット 2020-11-23 15.13.35.png
スクリーンショット 2020-11-23 15.16.02.png

最後に

レイアウトを管理することができるのがStack Viewの役割です。
これはかなり便利な機能だなと思いました。
まだこのくらいしか使い方知りませんが、また便利な使い方があれば更新していきます。

参考サイト

https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutWithoutConstraints.html#//apple_ref/doc/uid/TP40010853-CH8-SW1

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

WindowsとMacのショートカットキー対応表

執筆の動機

今後、学んだことをQiitaでアウトプットしていきたい。
しかし記事を作成した経験が無く、まずは投稿してみたかった。
そんな中、人生で初めてMacを購入しました。
Ctrlキーが無かったり、入力方式(かな、半角英数)を切り替える方法が分からなかったりと操作感が異なり興奮の連続です。新しいことを学ぶのって楽しいですよね。
Qiita初投稿には良い題材ではないかと考え、私がWindowsでよく使用するショートカットキーをMacではどのように使用するのか調べてまとめることにしました。

ショートカットキー対応表

操作 Windows Mac
テキストをコピー Ctrl + C command + C
コピーしたテキストを貼り付け Ctrl + V command + V
テキストを切り取り Ctrl + X command + X
操作を戻す Ctrl + Z command + Z
操作を戻すを取り消し Ctrl + Y command + Shift + Z
デスクトップ画面を表示 Windwos + D fn + F11
アクティブウィンドウの切り替え Alt + tab command + tab
画面をロック Windows + L command + control + Q
ウィンドウを最大化 Windows + ↑ command + control + F
入力方式切り替え 半角/全角 control + スペース

普段どのようなショートカットキーを使用しているかなかなか思い出せませんね。
記事作成の良い練習になりました。

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

U-2Netをつかう。

u2netqual.png

画像内の顕著なオブジェクトをセグメントしてくれるモデル。

手順

1、U2-NetのGitHubリポジトリをクローン
git clone https://github.com/NathanUA/U-2-Net.git
cd U-2-Net/
2、事前学習済みモデルをリポジトリのリンクからダウンロードし、指定のディレクトリに配置
mkdir saved_models/u2netp/
3、とりあえずPythonで試してみる。

リポジトリで配布されているモデルは174MBと4MBの2種類あって、軽量の方をつかう場合は、model_nameをu2netpに書き換えます。

あと、PytorchでデフォルトはCudaデバイス(GPU)をつかう設定になっているので、CPUで実行する場合はtorch.deviceを切り替えます

u2net_test.py
model_name='u2net'#u2netp
python u2net_test.py

4MBのモデルでこの性能。すごい。
im_21.pngim_21 (1).png

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium
ホームページ

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

PytorchモデルをONNX経由でCoreMLモデルに変換する(あえてね)

CoreMLTools4.0からPytorchモデルを直接(Traced_Model経由で)変換できるようになりました。
旧式のONNX経由で変換するやりかたは非推奨になったのだけど、とはいえ「直接変換はできない」かつ「旧式ONNX方式だと変換できた」ケースもあったので、書いておくです?

1.PyTorchモデルをONNXにエクスポート

x = torch.randn(1, 3, 224, 224, requires_grad=True).cuda() #ダミーインプット(batch_size,color_channel,width,height) モデルがCuda使ってるなら、.cuda()

torch.onnx.export(torch_model,   # モデル
                  x,   # ダミーインプット
                  "torch_model.onnx",   # 保存パス
                  export_params=True,   # モデルの重みも保存するか
                  # opset_version=10,   # エクスポートするONNXのバージョン
                  # do_constant_folding=True,  # 最適化のために定数畳み込みを実行するかどうか
                  # input_names = ['input'],   # the model's input names
                  # output_names = ['output'], # the model's output names
                  dynamic_axes={'input' : {0 : 'batch_size'},
                                'output' : {0 : 'batch_size'}})

2.CoreMLTools ONNX_CoreMLをインストール

pip install coremltools==3.4
pip install onnx_coreml

3.ONNXをCoreMLに変換

from onnx_coreml import convert

mlmodel = convert("torch_model.onnx")
mlmodel.save("torch_model.mlmodel")

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium
ホームページ

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