20200724のiOSに関する記事は10件です。

[iOS]COVID-19関連のアプリ審査Rejectリスク

内容

先日審査提出したiOSアプリがリジェクトされてしまった。

内容は

COVID-19パンデミックへの不適切な参照が含まれていますよ。
COVID-19パンデミックに焦点を当ててない、または関連していないレビューガイドラインに沿ったアプリにしてくれよ。
COVID-19パンデミックに対する追加情報は公式ドキュメントを見てね。

公式ドキュメント
Ensuring the Credibility of Health & Safety Information

知らなかった。(遅っ)

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

Swiftで指定した桁数のランダムな文字列を生成

備忘録:book:
ユニークなIDを生成したいときなどに

func generator(_ length: Int) -> String {
    let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    var randomString = ""
    for _ in 0 ..< length {
        randomString += String(letters.randomElement()!)
    }
    return randomString
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

目でTwitterとブラウザを操作する

この記事は...?

以前、「手を使わずにTwitterを閲覧したい!」という強い意志のもと、NoHandTwitterなるものを作りました。
↓↓↓
https://qiita.com/LilyMameoka/items/52f05eac7ae95b399e32

これが結構使えたんですよ。昼休みに昼食を食べながらTwitterを閲覧できましたし、あとは授業中でもTwitterを...おっと、失言。(開発当時)受験生だっただろ、おい。

で、授業中にTwitterをしていて思ったんですけど、前の記事の状態では先生が来たときにTwitterの画面が見えてしまうんですよね。「これではバレてしまう」、そう思ったんですわ。なんとかしたいですよね。

というわけで、今回は授業中にユースケースを絞り込んで、アップデートしました。

(これを作ったのは前回の記事の直後なのですが、最近「書いてなかったな」と思い出しまして今更ですが、綴りました。)

コードの全貌は記事の最後に掲載しました。

先生が来ちゃったらどーしよ(どーする?!)

まずは先生が来てもTwitterをやっていることがバレないためのアップデートです。授業中の机の上にスマホがある分には怒られませんから、画面を目だけで消しましょうか。

...と、簡単に「画面を目だけで消しましょうか」と言いましたが、コードで画面をロックしてスリープにする方法、調べても出てこないんですよ。コードでスリープさせることはできないんですかね。まぁ、サードパーティー製のよくわからないアプリに画面のスリープやロックを勝手にされるようでは危険ですし、仕方ないかな...。

今回は画面のスリープの代替策として、「画面全体を真っ黒なUIButtonで覆う」ことにしました。

ViewController.swift
    func createBlackButton() {
        blackButton = UIButton()
        blackButton.frame = view.frame
        blackButton.backgroundColor = UIColor.black
        blackButton.addTarget(self, action: #selector(blackButton_TouchUpInside), for: UIControl.Event.touchUpInside)
        view.addSubview(blackButton)
        blackButton.isHidden = true
    }
    @objc func blackButton_TouchUpInside() {
        blackButton.isHidden = true
    }

この関数createBlackButton()で画面全体を真っ黒なUIButtonで覆うもので、これはviewDidLoadで呼び出します。押すと非表示になります。

この「擬似スリープ」は両目を1.5秒以上閉じた際に起動します(非表示が解除される)。前回の記事で、"予め設定した文言をツイートする処理"をしていたところです。

ViewController.swift
    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {

        DispatchQueue.global().async {

//             ~中略~

            if (self.leftEye>self.judgeEyeClose)&&(self.rightEye>self.judgeEyeClose){
                self.closeTime+=1
                if(self.closeTime>self.amountOfFlame){
                    DispatchQueue.main.async{
                        self.blackButton.isHidden = false
                    }
                }
            } else {
                self.closeTime = 0
            }

        }

    }

.isHiddenっていうプロパティ、結構便利ですよね。

これで、授業中先生が巡回してきても目を閉じれば画面は真っ暗。ほら!何もしてませんよ〜〜!

画面をタップすればすぐに復帰できます。(ロックかけていないから、すぐTwitterに復帰できてむしろ良い)

目でブラウザを閲覧したい!

前回の記事ではTwitterの閲覧を主としたアプリにしていました。でも、実際に使ってみて、ブラウザも目で操作したいなぁ、と思いまして。授業中にブラウザで(授業に関連した)調べ物をしている分には(先生にもよるけど大抵は)怒られませんし。

これは簡単です。今まではWKWebViewにTwitterを表示させていたわけですから、読み込むURLをGoogleのトップページあたりにすればよいだけです。

...うーん。それだけではあまり面白くない、ですよね。

というわけで以下の機能を追加しました。

・顎を左に動かして「戻る」, 顎を右に動かして「進む」
・眉を上げるとリロード, 眉を顰めるとGoogleのトップページに戻る

さらに、Twitterもしたいので以下の機能も加えました。

・舌を出したらTwitterとブラウザを切り替える

これらの処理は以下のように実装しています。非常に単純です。Twitterを表示するWKWebViewはブラウザを表示するWKWebViewの上に重ねていて、舌を出すことで表示/非表示を切り替えています。

ViewController.swift
    func twitterSwitch() {
        DispatchQueue.main.async{
            if self.twitterView.isHidden {
                self.twitterView.isHidden = false
            } else {
                self.twitterView.isHidden = true
            }
        }
    }

    func createTwitterView() {
        twitterView = WKWebView(frame:CGRect(x:0, y:0, width:self.view.bounds.size.width, height:self.view.bounds.size.height))
        let urlString = "https://twitter.com/home"
        let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
        let url = NSURL(string: encodedUrlString!)
        let request = NSURLRequest(url: url! as URL)
        twitterView.load(request as URLRequest)
        self.view.addSubview(twitterView)
        twitterView.isHidden = true
    }

    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {

        DispatchQueue.global().async {

            guard let faceAnchor = anchor as? ARFaceAnchor else {
                return
            }

            if let lookUpLeft = faceAnchor.blendShapes[.eyeLookUpLeft] as? Float {
                if lookUpLeft > self.judgeEyeValUp {
                    self.scrollDownInMainThread()
                }
            }
            if let lookUpRight = faceAnchor.blendShapes[.eyeLookUpRight] as? Float {
                if lookUpRight > self.judgeEyeValUp {
                    self.scrollDownInMainThread()
                }
            }
            if let lookDownLeft = faceAnchor.blendShapes[.eyeLookDownLeft] as? Float {
                if lookDownLeft > self.judgeEyeValDown {
                    self.scrollUpInMainThread()
                }
            }
            if let lookDownRight = faceAnchor.blendShapes[.eyeLookDownRight] as? Float {
                if lookDownRight > self.judgeEyeValDown {
                    self.scrollUpInMainThread()
                }
            }

            if let leftEyeBlink = faceAnchor.blendShapes[.eyeBlinkLeft] as? Float{
                self.leftEye = leftEyeBlink
            }
            if let rightEyeBlink = faceAnchor.blendShapes[.eyeBlinkRight] as? Float{
                self.rightEye = rightEyeBlink
            }

            if let browUpAndDown = faceAnchor.blendShapes[.browInnerUp] as? Float{
                self.browVal = browUpAndDown
            }

            if let tongueOut = faceAnchor.blendShapes[.tongueOut] as? Float{
                self.tongueVal = tongueOut
            }

            if let jawLeft = faceAnchor.blendShapes[.jawLeft] as? Float{
                self.jawLeftVal = jawLeft
            }

            if let jawRight = faceAnchor.blendShapes[.jawRight] as? Float{
                self.jawRightVal = jawRight
            }

            if self.browVal < 0.05 {
                let urlString = "https://www.google.co.jp"
                let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
                let url = NSURL(string: encodedUrlString!)
                let request = NSURLRequest(url: url! as URL)
                self.webView.load(request as URLRequest)
            }

            if self.browVal > 0.4 {
                DispatchQueue.main.async{
                    self.webView.reload()
                }
            }

            if self.tongueVal > 0.08 {
                self.twitterSwitch()
            }

            if self.jawLeftVal > 0.1 {
                DispatchQueue.main.async{
                    self.webView.goForward()
                }
            }

            if self.jawRightVal > 0.1 {
                DispatchQueue.main.async{
                    self.webView.goBack()
                }
            }
//             ~中略~

        }

    }

このアプリをハッカソン的な感じの授業で発表するために紹介の動画を作ったのですが、これ使ってる時の顔の動き、到底他人に見せられないな...特にTwitterとブラウザの切り替えが...(その授業のプレゼンではちゃんと見せましたが!!)お嫁に行けない...

ソースコード

AppDelegate.swift、storyboardは何も弄っていません。info.plistも前回の記事のままです(多分)。

ViewController.swift
//
//  ViewController.swift
//  NoHandTwitter
//
//  Created by Lily.Mameoka on 2019/06/19.
//  Copyright © 2019 Lily.Mameoka. All rights reserved.
//

import UIKit
import WebKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate, WKUIDelegate{

    private let defaultConfiguration: ARFaceTrackingConfiguration = {
        let configuration = ARFaceTrackingConfiguration()
        return configuration
    }()

    var webView: WKWebView!
    var twitterView: WKWebView!
    var blackButton: UIButton!
    @IBOutlet var sceneView: ARSCNView!

    var judgeEyeValUp:Float = 0.3  //Please adjust!
    var judgeEyeValDown:Float = 0.3  //Please adjust!
    var closeTime:Float = 0
    var amountOfFlame:Float = 15
    var judgeEyeClose:Float = 0.8  //Please adjust!
    var leftEye:Float = 0
    var rightEye:Float = 0
    var browVal:Float = 0.1
    var tongueVal:Float = 0
    var jawLeftVal:Float = 0
    var jawRightVal:Float = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        createWebView()
        createTwitterView()
        createBlackButton()
        sceneView.delegate = self
    }

    func createWebView() {
        webView = WKWebView(frame:CGRect(x:0, y:0, width:self.view.bounds.size.width, height:self.view.bounds.size.height))
        let urlString = "https://www.google.co.jp"
        let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
        let url = NSURL(string: encodedUrlString!)
        let request = NSURLRequest(url: url! as URL)
        webView.load(request as URLRequest)
        self.view.addSubview(webView)
    }

    func createBlackButton() {
        blackButton = UIButton()
        blackButton.frame = view.frame
        blackButton.backgroundColor = UIColor.black
        blackButton.addTarget(self, action: #selector(blackButton_TouchUpInside), for: UIControl.Event.touchUpInside)
        view.addSubview(blackButton)
        blackButton.isHidden = true
    }

    @objc func blackButton_TouchUpInside() {
        blackButton.isHidden = true
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        sceneView.session.run(defaultConfiguration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        sceneView.session.pause()
    }


    func scrollUpInMainThread() {
        DispatchQueue.main.async {
            if self.twitterView.isHidden {
                self.webView.scrollView.contentOffset = CGPoint(x: 0, y: self.webView.scrollView.contentOffset.y + 10)
            } else {
                self.twitterView.scrollView.contentOffset = CGPoint(x: 0, y: self.twitterView.scrollView.contentOffset.y + 10)
            }

        }
    }

    func scrollDownInMainThread() {
        DispatchQueue.main.async {
            if self.twitterView.isHidden {
                self.webView.scrollView.contentOffset = CGPoint(x: 0, y: self.webView.scrollView.contentOffset.y - 10)
            } else {
                self.twitterView.scrollView.contentOffset = CGPoint(x: 0, y: self.twitterView.scrollView.contentOffset.y - 10)
            }
        }
    }

    func twitterSwitch() {
        DispatchQueue.main.async{
            if self.twitterView.isHidden {
                self.twitterView.isHidden = false
            } else {
                self.twitterView.isHidden = true
            }
        }
    }

    func session(_ session: ARSession, didFailWithError error: Error) {
    }

    func sessionWasInterrupted(_ session: ARSession) {
    }

    func sessionInterruptionEnded(_ session: ARSession) {
    }

    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {

        DispatchQueue.global().async {

            guard let faceAnchor = anchor as? ARFaceAnchor else {
                return
            }

            if let lookUpLeft = faceAnchor.blendShapes[.eyeLookUpLeft] as? Float {
                if lookUpLeft > self.judgeEyeValUp {
                    self.scrollDownInMainThread()
                }
            }
            if let lookUpRight = faceAnchor.blendShapes[.eyeLookUpRight] as? Float {
                if lookUpRight > self.judgeEyeValUp {
                    self.scrollDownInMainThread()
                }
            }
            if let lookDownLeft = faceAnchor.blendShapes[.eyeLookDownLeft] as? Float {
                if lookDownLeft > self.judgeEyeValDown {
                    self.scrollUpInMainThread()
                }
            }
            if let lookDownRight = faceAnchor.blendShapes[.eyeLookDownRight] as? Float {
                if lookDownRight > self.judgeEyeValDown {
                    self.scrollUpInMainThread()
                }
            }

            if let leftEyeBlink = faceAnchor.blendShapes[.eyeBlinkLeft] as? Float{
                self.leftEye = leftEyeBlink
            }
            if let rightEyeBlink = faceAnchor.blendShapes[.eyeBlinkRight] as? Float{
                self.rightEye = rightEyeBlink
            }

            if let browUpAndDown = faceAnchor.blendShapes[.browInnerUp] as? Float{
                self.browVal = browUpAndDown
            }

            if let tongueOut = faceAnchor.blendShapes[.tongueOut] as? Float{
                self.tongueVal = tongueOut
            }

            if let jawLeft = faceAnchor.blendShapes[.jawLeft] as? Float{
                self.jawLeftVal = jawLeft
            }

            if let jawRight = faceAnchor.blendShapes[.jawRight] as? Float{
                self.jawRightVal = jawRight
            }

            if self.browVal < 0.05 {
                let urlString = "https://www.google.co.jp"
                let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
                let url = NSURL(string: encodedUrlString!)
                let request = NSURLRequest(url: url! as URL)
                self.webView.load(request as URLRequest)
            }

            if self.browVal > 0.4 {
                DispatchQueue.main.async{
                    self.webView.reload()
                }
            }

            if self.tongueVal > 0.08 {
                self.twitterSwitch()
            }

            if self.jawLeftVal > 0.1 {
                DispatchQueue.main.async{
                    self.webView.goForward()
                }
            }

            if self.jawRightVal > 0.1 {
                DispatchQueue.main.async{
                    self.webView.goBack()
                }
            }

            if (self.leftEye>self.judgeEyeClose)&&(self.rightEye>self.judgeEyeClose){
                self.closeTime+=1
                if(self.closeTime>self.amountOfFlame){
                    DispatchQueue.main.async{
                        self.blackButton.isHidden = false
                    }
                }
            } else {
                self.closeTime = 0
            }

        }

    }

    func createTwitterView() {
        twitterView = WKWebView(frame:CGRect(x:0, y:0, width:self.view.bounds.size.width, height:self.view.bounds.size.height))
        let urlString = "https://twitter.com/home"
        let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
        let url = NSURL(string: encodedUrlString!)
        let request = NSURLRequest(url: url! as URL)
        twitterView.load(request as URLRequest)
        self.view.addSubview(twitterView)
        twitterView.isHidden = true
    }

}

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

Twitterの横スワイプメニューのデザインの作り方

はじめに

今回はTwitterアプリの検索タブにあるカテゴリー分けニュースフィードのデザインを紹介します。これはNewsPicksやメルカリでも利用されている、ユーザーエクスペリエンス向上にはもってこいの機能です。ちなみに私の作ったフィットネスアプリ『モニトレ』でもある機能です。

twitter_feed.gif

*ストーリーボードは使用していないので、Extension.swiftファイルに制約のルールを設定しています。(ステップ3をご覧ください)

開発環境

Swift 5.2.4
Xcode 11.5(Deployment Target 13.0)
ストーリーボードなし

ストラクチャー

構成は以下の様になっています。
twitter_feed_structure.png

ステップ1: 初期設定

SceneDelegate.swift内に初期ページ設定をします。初期ページはHomeFeedContentViewとします

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let homeFeedContentView = HomeFeedContentView()
        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: homeFeedContentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

ステップ2: SWiftUIからUICollectionViewControllerを表示する

以下を実装するとUIKitでもホットリロード機能(canvas)が使えるため一々Runしなくてもいいので便利です。

class HomeFeedController: UICollectionViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.backgroundColor = .lightGray
    }
}
struct HomeFeedIntegratedController: UIViewControllerRepresentable {

    func makeUIViewController(context: UIViewControllerRepresentableContext<HomeFeedIntegratedController>) -> HomeFeedController {
        return HomeFeedController(collectionViewLayout: UICollectionViewFlowLayout())
    }

    func updateUIViewController(_ uiViewController: HomeFeedController, context: Context) {

       }
}


struct HomeFeedContentView: View {
    var body: some View {
        HomeFeedIntegratedController().edgesIgnoringSafeArea(.all)
    }
}

struct HomeFeedContentView_Preview: PreviewProvider {
    static var previews: some View {
        HomeFeedContentView()
    }
}

ステップ3: トップメニューバー作り

トップに配置するメニューバーを作成します。

import UIKit
import SwiftUI

protocol TopBarMenuControllerDelegate {
    func didTapMenu(indexPath: IndexPath)
}

class TopBarMenuController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    var topBarMenuControllerDelegate: TopBarMenuControllerDelegate?

    fileprivate let menuCellId = "menuCellId"
     fileprivate let menuItem = ["おすすめ", "トレンド", "ニュース", "エンタメ"]

    //メニューバー内の青いライン
    let menuBottomLine: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBlue
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.backgroundColor = .darkGray
        collectionView.register(TopBarCell.self, forCellWithReuseIdentifier: menuCellId)
        collectionView.alwaysBounceHorizontal = true

        //横にスクロールするための機能
        if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
            layout.minimumInteritemSpacing = 0
        }

        view.addSubview(menuBottomLine)
        menuBottomLine.anchor(top: nil, left: view.leftAnchor, bottom: view.bottomAnchor, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, height: 7, width: 0)
        menuBottomLine.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1 / 4).isActive = true
    }

    //menuItem配列の数を返す
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return menuItem.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: menuCellId, for: indexPath) as! TopBarCell
        cell.menuLabel.text = menuItem[indexPath.item]
        return cell
    }
    //それぞれのセルサイズ(width)はmenuItem配列の数に合わせる = CGSize(width: view.frame.width / 4, height: view.frame.height)
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width / 4, height: view.frame.height)
    }

    //メニューアイテムをタップした時の機能を追加するためにプロトコールを宣言
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        topBarMenuControllerDelegate?.didTapMenu(indexPath: indexPath)
    }

}

class TopBarCell: UICollectionViewCell {

    let menuLabel: UILabel = {
        let label = UILabel()
        label.font = .boldSystemFont(ofSize: 16)
        label.textColor = .white
        label.text = "Menu"
        label.textAlignment = .center
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupMenuView()
    }

    fileprivate func setupMenuView() {
        addSubview(menuLabel)
        menuLabel.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, height: 20, width: 0)
        menuLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        menuLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

struct TopBarIntegratedController: UIViewControllerRepresentable {

    func makeUIViewController(context: UIViewControllerRepresentableContext<TopBarIntegratedController>) -> TopBarMenuController {
        return TopBarMenuController(collectionViewLayout: UICollectionViewFlowLayout())
    }

    func updateUIViewController(_ uiViewController: TopBarMenuController, context: Context) {

       }
}


struct TopBarMenuContentView: View {
    var body: some View {
        TopBarIntegratedController().edgesIgnoringSafeArea(.all)
    }
}

struct TopBarMenuContentView_Preview: PreviewProvider {
    static var previews: some View {
        TopBarMenuContentView()
    }
}

制約ルールは別ファイルExtensions.swiftで設定しています。

//Extensions.swift
import Foundation
import UIKit

extension UIView {
func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?,  bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, height: CGFloat, width: CGFloat){

    translatesAutoresizingMaskIntoConstraints = false

    if let top = top {
        self.topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
    }

    if let left = left {
        self.leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
    }

    if let bottom = bottom {
        bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
    }

    if let right = right {
        rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
    }

    if width != 0 {
        widthAnchor.constraint(equalToConstant: width).isActive = true
    }

    if height != 0 {
        heightAnchor.constraint(equalToConstant: height).isActive = true
    }
    }
}

ステップ4: メニューアイテムの作成とメニューバーの動き

メニューアイテム[おすすめ、トレンド,ニュース、スポーツ]のテキストと背景の色を指定するためにFeedというクラスを作成。最後にメニューバーの動きを加えます。

import UIKit
import SwiftUI

//メニューアイテムの詳細クラスを作成
class Feed {
    let text: String
    let backgroundColor: UIColor

    init(text: String, backgroundColor: UIColor) {
        self.text = text
        self.backgroundColor = backgroundColor
    }
}

class HomeFeedController: UICollectionViewController, TopBarMenuControllerDelegate, UICollectionViewDelegateFlowLayout {

    fileprivate let menuCellId = "menuCellId"

    var feeds = [Feed]()

    fileprivate let topBarMenuController = TopBarMenuController(collectionViewLayout: UICollectionViewFlowLayout())

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.backgroundColor = .lightGray
        collectionView.register(HomeFeedCell.self, forCellWithReuseIdentifier: menuCellId)

        collectionView.isPagingEnabled = true

        if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
            layout.minimumInteritemSpacing = 0
        }

        topBarMenuController.topBarMenuControllerDelegate = self

        setupTopBarMenuController()

        feeds = [Feed(text: "おすすめ画面", backgroundColor: .systemRed), Feed(text: "トレンド画面", backgroundColor: .systemTeal), Feed(text: "ニュース画面", backgroundColor: .systemOrange), Feed(text: "エンタメ画面", backgroundColor: .systemGreen)]
    }
    //画面を横にスクロールした時にメニューバー内の下枠(青)が付いてくる仕様
    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let x = scrollView.contentOffset.x
        let offset = x / 4
        topBarMenuController.menuBottomLine.transform = CGAffineTransform(translationX: offset, y: 0)
    }

    override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let x = targetContentOffset.pointee.x
        let item = x / view.frame.width
        let indexPath = IndexPath(item: Int(item), section: 0)
        topBarMenuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
    }

    //ステップ3で宣言したプロトコールを実装
    //メニューアイテムをタップした時に画面がスライドする機能
    func didTapMenu(indexPath: IndexPath) {
        collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)

    }
    //feeds配列の数を返す
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return feeds.count
    }

    //セルにそれぞれのテキスト背景色を設定する
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: menuCellId, for: indexPath) as! HomeFeedCell
        cell.backgroundColor = feeds[indexPath.item].backgroundColor
        cell.titleLabel.text = feeds[indexPath.item].text
        return cell
    }

    fileprivate func setupTopBarMenuController() {
        //ナビゲーションバーの高さと色を設定
        let navBarController = UINavigationBar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 72))
        view.addSubview(navBarController)
        navBarController.barTintColor = .darkGray

        //TopBarMenuControllerを設置する
        view.addSubview(topBarMenuController.view)
        topBarMenuController.view.anchor(top: navBarController.bottomAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, height: 60, width: 0)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width, height: view.frame.height)
    }
}

class HomeFeedCell: UICollectionViewCell {

    let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "タイトル"
        label.font = .boldSystemFont(ofSize: 16)
        label.textColor = .white
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        addSubview(titleLabel)
        titleLabel.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, height: 0, width: 0)
        titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

struct HomeFeedIntegratedController: UIViewControllerRepresentable {

    func makeUIViewController(context: UIViewControllerRepresentableContext<HomeFeedIntegratedController>) -> HomeFeedController {
        return HomeFeedController(collectionViewLayout: UICollectionViewFlowLayout())
    }

    func updateUIViewController(_ uiViewController: HomeFeedController, context: Context) {

       }
}


struct HomeFeedContentView: View {
    var body: some View {
        HomeFeedIntegratedController().edgesIgnoringSafeArea(.all)
    }
}

struct HomeFeedContentView_Preview: PreviewProvider {
    static var previews: some View {
        HomeFeedContentView()
    }
}

最後に

今回はデザインのみですが、実際にはカテゴリー別の情報(APIなど)ををそれぞれのページに表示させます。
次の記事はそれを実装しようと思いますが、何がいいですかね?
何かリクエストがあればお気軽にコメントからどうぞ!

現在、私の制作した体験/遊び/学びが楽しめるアプリ『WalCal』でジャンケンアプリ制作体験を掲載しています。

ご興味ある方は是非チェックして見てください!

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

大学生が1週間でFlutterアプリを学んでリリースした過程(6日目)

こんにちはシオンです。

今日で6日目、残すところあと2日となりました。
計画通りに行けば、コードをかけるのは今日が最後の日です。現状としては5画面あるうちの2画面の画面構成まで、処理はひとつもかいていなく、また書き方もわからないため、進捗率としては5%...よくて8%ほどでしょうか。

やばいですね。でも期限はもう決まってしまっているのでやり切りましょう。途中までで終わらないよりも、終わらせる。今回は60点でもいいから終わらせることが大事なのです。

では、時間がないので(本当に!)やっていきます。

目次

■実装する処理の確認
■画面遷移を学ぶ
■残りの画面を作って画面遷移のルートを繋ぐ
■まとめ

■実装する処理の確認

昨日3画面の実装をしてFlutterでの画面の配置に関してはなんとなくわかってきました。
残り3画面は処理を実装するのと同時にプログラミングしていきます。
今回実装する残りの処理としては
・画面遷移
・入力した文字を読み取って表示
・カウントアップ
この3つです。

実装するためにはどうやって実装すればいいのかを知らなければいけないので、やり方を学んでいきましょう。

■画面遷移を学ぶ

まず、画面遷移をするためには遷移元の画面と遷移先の画面が必要で、これを行き来することが画面遷移になります。
Flutterではこの画面のことを「ルート(route)」、ルート同士の行き来のことを「ナビゲーション 」というらしいです。
ルートはもう用意できているので、画面遷移を実装するためにはナビゲーションを学べば良さそうです。

見てみたところ、次の画面に行くためにはNavigator.push、前の画面に戻るためにはNavigator.popというメソッドを呼び出せば良いそうです。
つまり、画面遷移をしたいタイミングでボタンを押した時にこのメソッドを呼び出してあげれば完了ですね。
早速やってみます。

1画面目から
1.png

2画面目へ
2.png

画像だと全く遷移してるからわからないですね。。
でもできています。しっかりと。
想像してください。
最初の画面で、愚痴を吐き出すボタンを押すと右にスライドして画面遷移し吐き出す画面に、そして左上の矢印をタップすると左にスライドして画面遷移しました。
ぜひ想像してください。(私には画面が遷移する瞬間をスクショで捉えるスキルも時間もありませんでした。)

これで画面遷移ができるようになりました!素直に嬉しいです。プログラミング楽しい笑

■残りの画面を作って画面遷移のルートを繋ぐ

画面遷移ができたので、最後の画面まで作って一旦表示だけ実装してしまいましょう。

まず3画面目
3.png

次の画面へ
4.png

そしてラスト
5.png

これで全ての画面を移動することができるようになりました。よしよし。

あとは細かな機能の実装をしていきます。
実装する機能としては
・2画面目でストレスLvを表示
・3画面目に2画面目で入力した愚痴タイトルを表示
・乗り越えた数のカウントと保存
これを実装すれば晴れて完成です!

■まとめ

なんとか見えてきましたね。ゴールが。
ただ、すみません。明日の朝がどうしても早いので今日はここまでにしておきます。

リリースはできなくても申請は絶対にします。
そして間に合わなかった理由をAppleとGoogleのせいにします。

明日ラストやり切りましょう!

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

Swift初心者がJSON解析をやってみる

JSON解析ってかっこいい響き!

JSON解析って言葉かっこいい。賢そう!
APIとかもよく分からない。そんなswift歴1週間の私がSwiftを使ってJSON解析をやりますよ〜
ここではpixabayってサイトから画像を取ってきます。

pixabayでAPIの仕様書を確認しよう。

pixabay←こちらどす
サインアップする必要があります。

image.png

右上のExploreってとこからAPIってとこ押すと、

image.png

こんなのが出るからGetStartedを押しちゃう

スクリーンショット 2020-07-24 18.58.22.png
ちょっとしたにスクロールするとkeyと書かれているのがある。APIを叩く時にはここのkeyを指定してアクセスする必要がある。

image.png

APIを叩いて返ってくるJSONはこんな感じ。13行目のWebFormatURLがお目当ての画像のURL。こいつをもらうよ!

準備からはじめよう

まずは準備から。cocoapodというものをインストールする。
https://qiita.com/ShinokiRyosei/items/3090290cb72434852460
そして、Alamofire, SwiftyJSON, SDWebImageをcocoaposdを使ってインストールしましょう。

じゃあ実際にコード書こうね。

//画像を扱うのもあってこいつらをインポートするぜ!
import UIKit
import Alamofire
import SwiftyJSON
import SDWebImage
import Photos

class ViewController: UIViewController {
    //テキストフィールド。ここに入れたキーワードに関する画像を取得するぜ!
    @IBOutlet weak var searchTextField: UITextField!
    //画像を表示するイメージビュー
    @IBOutlet weak var imageView: UIImageView!

    //テキストフィールドに文言を入れてボタンをポチッとやった時。入力された文言で検索した画像を取得するよん
    @IBAction func searchAction(_ sender: Any) {
        if searchTextField.text == "" {
            //何も文言が入っていなかった時、一旦dogで検索するよ。
            getImages(keyword: "dog")
        } else {
            //文言が入っていたらそのキーワードで画像を取得してくるわ!
            getImages(keyword: searchTextField.text!)
        }    
    }

    //検索キーワードをもとに画像を取得するメソッドを読んでる。
    func getImages(keyword: String) {
        //ここにアクセスしたら画像あげるよ!!っていうURLをここに書く。sampleって書いてあるところはkeyで置き換えてくれよな!
        let url = "https://pixabay.com/api/?key=sample=\(keyword)"
        //Alamofireを使ってAPIリクエストを出す。Alamofireについては詳しくは知らない。
        //下のAFはAlmofireの略。このurlに対してGETメソッドでJSON形式でリクエストするで!っていってる(多分)。そんで返ってきたレスポンスに対する処遇をクロージャーで記述する!
        AF.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON {(response) in
            switch(response.result) {
            //API通信成功した時〜
            case.success:
                //返ってきたJSONを変数にぶち込むで!
                let json:JSON = JSON(response.data as Any)
                //JSONの中から欲しい情報だけ抽出
                var imageString = json["hits"][0]["webformatURL"].string
                //画面に表示しまっせ。
                self.imageView.sd_setImage(with: URL(string: imageString!), completed: nil)
            //失敗した時〜
            case.failure(let error):
                //失敗したらエラー文が返ってくるのでそれを表示しちゃう。
                print(error)
            }
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリをApp Storeに公開する手順を簡潔に解説

App StoreにiOSアプリを公開する手順について、
必要事項のみ抜粋して簡潔に解説してみました。

やること一覧

① 事前準備

 1. iOSアプリの作成
 2. App Store公開用のアイコン作成
 3. App Store公開用のスクリーンショット作成
 4. プライバシーポリシー用のサイト作成
 5. サポートURLに記入するための連絡用ホームページの作成

② Apple Developer Programへの参加

③ App Identifier(App ID)の作成

④ 証明書(Certificate)の作成

⑤ プロビジョニングプロファイル(Provisioning profile)の作成

⑥ App Store Connect にアプリの情報を登録

⑦ アプリのアップロード

⑧ 審査へ提出〜提出後の対応

① 事前準備

公開前にやっておくべき事前準備は下記の通りです。
申請中に足りない!!と気づいたら申請が遠回しになってしまうので、
事前に済ませておくのがベストです。

  1. iOSアプリの作成
    → 公開するiOSアプリを作成しましょう

  2. App Store公開用のアイコン作成
    → 1024×1024の透過なし画像で作成します。
     ※ 透過ありだと App Store Connect にアップロードする際にエラーになるので注意

  3. App Store公開用のスクリーンショット作成
    →「5.5インチ」「6.5インチ」「12.9インチ(iPad)」のそれぞれのスクリーンショットを
     最低2枚必要とするので、アプリが完成したら作成しましょう。

  4. プライバシーポリシー用のサイト作成
    → プライバシーポリシーを記載したWebサイトを作成しておく。

  5. サポートURLに記入するための連絡用ホームページの作成
    → デベロッパーと連絡が取れるために必要のため、Twitterのアカウント等でも可

② Apple Developer Programへの参加

下記のリンクの「登録」から参加することができます。
支払いはクレジットカード または iTunes Cardでもお支払いは可能です。
https://developer.apple.com/jp/programs/

③ App Identifier(App ID)の作成

App IDとは、アプリを識別するためのIDのことです。

▼ Certifidate, Identifiers & Profiles を選択 ▼
1.png

▼ 左のメニューから「Identifiers」を選択して、「+」ボタンを押下 ▼
2.png

▼ 「App IDs」を選択して「Continue」を押下 ▼
3.png

▼ 「App」を選択して、「Continue」を押下 ▼
4.png

▼ 「Description」と「Bundle ID」を記入して「Continue」を押下 ▼
DescriptionはApp IDの説明文(自分用なのでわかりやすいように記述)
Bundle IDはアプリのBundle IDを記入
5.png

▼ App IDが追加されたことを確認 ▼
6.png

④ 証明書(Certificate)の作成

証明書を登録し、Appleからの認証を得ることで、
プロビジョニングプロファイルの作成が可能になります。

▼ 証明書要求ファイルを作成する ▼
キーチェーンを起動して「証明書アシスタント」→「認証局に証明書を要求」を選択
1.png

▼ 証明書情報に「ユーザーのメールアドレス」「通称」を入力して、「ディスクに保存」を選び「続ける」を押下して、任意の場所に保存 ▼
ユーザーのメールアドレス: 開発者のメールアドレス
通称: わかりやすい名前(記入していれば何でも良い)
「CertificateSigningRequest.certSigningRequest」というファイルが保存される
2.png

▼ 左のメニューから「Certificates」を選択して「+」ボタンを押下 ▼
3.png

▼ 「iOS Distribution(App Store and Ad Hoc)」を選択して「Continue」を押下 ▼
4.png

▼ 「Choose File」を選択して先ほど作成した「証明書要求ファイル(CertificateSigningRequest.certSigningRequest)」を選択して「Continue」を押下 ▼
5.png

▼ 「Download」押下して証明書をダウンロード ▼
「ios_distribution.cer」というファイルがダウンロードされる
6.png

▼ 証明書をキーチェーンに保存 ▼
ダウンロードされた証明書(ios_distribution.cer)をダブルクリックするとキーチェーンに証明書が登録されます。
7.png

⑤ プロビジョニングプロファイル(Provisioning profile)の作成

プロビジョニングプロファイル(Provisioning profile)とは、App IDと証明書(Certificate)を紐づけるものです。Xcodeに登録することでアプリの申請が可能になります。

▼ 左のメニューから「Profiles」を選択し「+」ボタンを押下 ▼
1.png

▼ Distribution > 「App Store」を選択して「Continue」を押下 ▼
2.png

▼ 「App ID」から指定のApp IDを選択して、「Continue」ボタンを押下 ▼
3.png

▼ 先ほど作成した証明書を選択して「Continue」ボタンを押下 ▼
4.png

▼ 「Provisioning Profile Name」を記入して「Continue」ボタンを押下 ▼
「Provisioning Profile Name」はわかりやすい任意の名前で良いです。
5.png

▼ 「Download」ボタンを押下して、プロビジョニングプロファイルをダウンロード ▼
6.png

▼ プロビジョニングプロファイルをXcodeに登録 ▼
ダウンロードしたプロビジョニングプロファイルをダブルクリックすると、Xcodeに登録されます。

⑥ App Store Connect にアプリの情報を登録

▼ App Store Connectにログイン ▼
下記のリンクからログインできます。
https://itunesconnect.apple.com/login

▼ 「マイApp」を押下 ▼
1.png

▼ 左上の「+」ボタンを押下して「新規App」を選択 ▼
2.png

▼ 新規アプリの情報を入力する ▼
プラットフォーム: iOSをチェック
名前: アプリ名を入力
プライマリ言語: 日本で配信するなら「日本語」を選択
バンドルID: アプリのBundle IDを選択
SKU: 任意の英数字を入力(アプリ名が良い)
ユーザーアクセス: アクセス制限なし
3.png

作成ボタンを押下すると、下記のような画面になるので、必要な情報を入力していく。
今回は、審査に提出する際に必須な情報のみを抜粋して説明していきます。
赤枠で囲った箇所の設定が審査に提出する際に必要な項目です。
4.png

iOS App > 1.0 提出準備中

  1. バージョン情報 > Appプレビューとスクリーンショット
    → 事前準備で作成したスクリーンショットを、各端末に応じた箇所にD&Dしましょう

  2. バージョン情報 > 概要
    → アプリの紹介文を記入しましょう

  3. バージョン情報 > キーワード
    → アプリを検索する際に使用されるキーワードを設定します。
     複数記入する際はカンマで区切って記入します。 例) ゲーム, RPG, Game

  4. バージョン情報 > サポートURL
    → 事前準備で作成した連絡用ホームページを記入します

  5. ビルド
    → アプリのアップロード後に「+」ボタンが表示されるので押下して、
     審査に提出するビルドバージョンを選択する。
     アプリのアップロード方法は後で解説するため後回しとします。

  6. App一般情報 > 著作権
    → 著作権を記入します 例)2020 Tanaka Taro

  7. バージョン情報 > 年齢制限指定
    → 年齢制限指定の右にある「編集」ボタンから設定できます。

  8. App Reviewに関する情報にて「サインインが必要です」のチェックを外す

  9. 広告ID(IDFA)で「いいえ」を選択

右上の「保存」ボタンを押下して変更した内容を保存

App情報

  1. プライバシーポリシーURL
    → 事前準備で作成したプライバシーポリシーが記載されたサイトのURLを記入します。

  2. カテゴリの選択

  3. コンテンツ配信権の右の「編集」ボタンを押下して設定する。

右上の「保存」ボタンを押下して変更した内容を保存

価格および配信状況

  1. 価格表から「JPY 0(無料)」を選択します。

右上の「保存」ボタンを押下して変更した内容を保存

⑦ アプリのアップロード

▼ Xcodeからプロジェクトを開き Product > Archive を選択 ▼
_00002.png

▼ Archiveが終了すると、下記の画面になるので、「Validate App」を押下 ▼
_00004.png

▼ そのまま「Next」を押下 ▼
_00005.png

▼ 「Automatically manage signing」を選択して「Next」を押下 ▼
_00006.png

▼ 「Validate」を押下 ▼
_00007.png

▼ 成功したので「Done」を押下 ▼
_00008.png

▼ Statusが「Validated」になっているので「Distribute App」を押下 ▼
_00009.png

▼ 「App Store Connect」を選択して「Next」を押下 ▼
_00010.png

▼ 「Upload」を選択して「Next」を押下 ▼
_00011.png

▼ そのまま「Next」を押下 ▼
_00012.png

▼ 「Automatically manage signing」を選択して「Next」を押下 ▼
_00013.png

▼ 「Upload」を押下 ▼
_00014.png

▼ アップロード中(3〜5分ぐらい) ▼
_00015.png

▼ 成功して「Done」を押下 ▼
_00016.png

⑧ 審査へ提出〜提出後の対応

  1. ビルドの追加
    アップロードが無事完了すると、20分〜30分ぐらいで、
    App Store Connect の 1.0 提出準備中 > ビルドの横に「+」ボタンが表示されるので、
    先ほどアップロードしたアプリを選択する。
    その際に輸出コンプラインスの回答をしなければならないが、暗号化をしないのであれば「いいえ」で
    暗号化をしているのであれば「はい」を選択して、アプリの内容に適した回答をしていきましょう。

  2. 審査へ提出
    1.0 提出準備中 の右上の「保存」ボタンの横に「審査へ提出」ボタンがあるので押下します。
    これでステータスが「審査待ち」になり、遅くとも3日ほどで審査が開始されます。

  3. 審査後の対応
    リジェクトされた内容が、アプリの修正を伴いような内容だった場合は、
    アプリを再ビルドして、先ほどのアップロード手順を再度行う必要がありますが、
    その際は、アプリのビルドバージョンを上げてアップロードする必要があります。
    ※ 筆者はビルドバージョンを1.0 → 1.0.1 → 1.0.2 のように上げています。
    _00017.png

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

【2020年度版】 iOSアプリをApp Storeに公開する手順を簡潔に解説

App StoreにiOSアプリを公開する手順について、
必要事項のみ抜粋して簡潔に解説してみました。
この手順通りにやれば無事公開できるはずです!
スクリーンショットも付けてより分かりやすさを追及しました。

やること一覧

① 事前準備

 1. iOSアプリの作成
 2. App Store公開用のアイコン作成
 3. App Store公開用のスクリーンショット作成
 4. プライバシーポリシー用のサイト作成
 5. サポートURLに記入するための連絡用ホームページの作成

② Apple Developer Programへの参加

③ App Identifier(App ID)の作成

④ 証明書(Certificate)の作成

⑤ プロビジョニングプロファイル(Provisioning profile)の作成

⑥ App Store Connect にアプリの情報を登録

⑦ アプリのアップロード

⑧ 審査へ提出〜提出後の対応

① 事前準備

公開前にやっておくべき事前準備は下記の通りです。
申請中に足りない!!と気づいたら申請が遠回しになってしまうので、
事前に済ませておくのがベストです。

  1. iOSアプリの作成
    → 公開するiOSアプリを作成しましょう

  2. App Store公開用のアイコン作成
    → 1024×1024の透過なし画像で作成します。
     ※ 透過ありだと App Store Connect にアップロードする際にエラーになるので注意

  3. App Store公開用のスクリーンショット作成
    →「5.5インチ」「6.5インチ」「12.9インチ(iPad)」のそれぞれのスクリーンショットを
     最低2枚必要とするので、アプリが完成したら作成しましょう。

  4. プライバシーポリシー用のサイト作成
    → プライバシーポリシーを記載したWebサイトを作成しておく。

  5. サポートURLに記入するための連絡用ホームページの作成
    → デベロッパーと連絡が取れるために必要のため、Twitterのアカウント等でも可

② Apple Developer Programへの参加

下記のリンクの「登録」から参加することができます。
支払いはクレジットカード または iTunes Cardでもお支払いは可能です。
https://developer.apple.com/jp/programs/

③ App Identifier(App ID)の作成

App IDとは、アプリを識別するためのIDのことです。

▼ Certifidate, Identifiers & Profiles を選択 ▼
1.png

▼ 左のメニューから「Identifiers」を選択して、「+」ボタンを押下 ▼
2.png

▼ 「App IDs」を選択して「Continue」を押下 ▼
3.png

▼ 「App」を選択して、「Continue」を押下 ▼
4.png

▼ 「Description」と「Bundle ID」を記入して「Continue」を押下 ▼
DescriptionはApp IDの説明文(自分用なのでわかりやすいように記述)
Bundle IDはアプリのBundle IDを記入
5.png

▼ App IDが追加されたことを確認 ▼
6.png

④ 証明書(Certificate)の作成

証明書を登録し、Appleからの認証を得ることで、
プロビジョニングプロファイルの作成が可能になります。

▼ 証明書要求ファイルを作成する ▼
キーチェーンを起動して「証明書アシスタント」→「認証局に証明書を要求」を選択
1.png

▼ 証明書情報に「ユーザーのメールアドレス」「通称」を入力して、「ディスクに保存」を選び「続ける」を押下して、任意の場所に保存 ▼
ユーザーのメールアドレス: 開発者のメールアドレス
通称: わかりやすい名前(記入していれば何でも良い)
「CertificateSigningRequest.certSigningRequest」というファイルが保存される
2.png

▼ 左のメニューから「Certificates」を選択して「+」ボタンを押下 ▼
3.png

▼ 「iOS Distribution(App Store and Ad Hoc)」を選択して「Continue」を押下 ▼
4.png

▼ 「Choose File」を選択して先ほど作成した「証明書要求ファイル(CertificateSigningRequest.certSigningRequest)」を選択して「Continue」を押下 ▼
5.png

▼ 「Download」押下して証明書をダウンロード ▼
「ios_distribution.cer」というファイルがダウンロードされる
6.png

▼ 証明書をキーチェーンに保存 ▼
ダウンロードされた証明書(ios_distribution.cer)をダブルクリックするとキーチェーンに証明書が登録されます。
7.png

⑤ プロビジョニングプロファイル(Provisioning profile)の作成

プロビジョニングプロファイル(Provisioning profile)とは、App IDと証明書(Certificate)を紐づけるものです。Xcodeに登録することでアプリの申請が可能になります。

▼ 左のメニューから「Profiles」を選択し「+」ボタンを押下 ▼
1.png

▼ Distribution > 「App Store」を選択して「Continue」を押下 ▼
2.png

▼ 「App ID」から指定のApp IDを選択して、「Continue」ボタンを押下 ▼
3.png

▼ 先ほど作成した証明書を選択して「Continue」ボタンを押下 ▼
4.png

▼ 「Provisioning Profile Name」を記入して「Continue」ボタンを押下 ▼
「Provisioning Profile Name」はわかりやすい任意の名前で良いです。
5.png

▼ 「Download」ボタンを押下して、プロビジョニングプロファイルをダウンロード ▼
6.png

▼ プロビジョニングプロファイルをXcodeに登録 ▼
ダウンロードしたプロビジョニングプロファイルをダブルクリックすると、Xcodeに登録されます。

⑥ App Store Connect にアプリの情報を登録

▼ App Store Connectにログイン ▼
下記のリンクからログインできます。
https://itunesconnect.apple.com/login

▼ 「マイApp」を押下 ▼
1.png

▼ 左上の「+」ボタンを押下して「新規App」を選択 ▼
2.png

▼ 新規アプリの情報を入力する ▼
プラットフォーム: iOSをチェック
名前: アプリ名を入力
プライマリ言語: 日本で配信するなら「日本語」を選択
バンドルID: アプリのBundle IDを選択
SKU: 任意の英数字を入力(アプリ名が良い)
ユーザーアクセス: アクセス制限なし
3.png

作成ボタンを押下すると、下記のような画面になるので、必要な情報を入力していく。
今回は、審査に提出する際に必須な情報のみを抜粋して説明していきます。
赤枠で囲った箇所の設定が審査に提出する際に必要な項目です。
4.png

iOS App > 1.0 提出準備中

  1. バージョン情報 > Appプレビューとスクリーンショット
    → 事前準備で作成したスクリーンショットを、各端末に応じた箇所にD&Dしましょう

  2. バージョン情報 > 概要
    → アプリの紹介文を記入しましょう

  3. バージョン情報 > キーワード
    → アプリを検索する際に使用されるキーワードを設定します。
     複数記入する際はカンマで区切って記入します。 例) ゲーム, RPG, Game

  4. バージョン情報 > サポートURL
    → 事前準備で作成した連絡用ホームページを記入します

  5. ビルド
    → アプリのアップロード後に「+」ボタンが表示されるので押下して、
     審査に提出するビルドバージョンを選択する。
     アプリのアップロード方法は後で解説するため後回しとします。

  6. App一般情報 > 著作権
    → 著作権を記入します 例)2020 Tanaka Taro

  7. バージョン情報 > 年齢制限指定
    → 年齢制限指定の右にある「編集」ボタンから設定できます。

  8. App Reviewに関する情報にて「サインインが必要です」のチェックを外す

  9. 広告ID(IDFA)で「いいえ」を選択

右上の「保存」ボタンを押下して変更した内容を保存

App情報

  1. プライバシーポリシーURL
    → 事前準備で作成したプライバシーポリシーが記載されたサイトのURLを記入します。

  2. カテゴリの選択

  3. コンテンツ配信権の右の「編集」ボタンを押下して設定する。

右上の「保存」ボタンを押下して変更した内容を保存

価格および配信状況

  1. 価格表から「JPY 0(無料)」を選択します。

右上の「保存」ボタンを押下して変更した内容を保存

⑦ アプリのアップロード

▼ Xcodeからプロジェクトを開き Product > Archive を選択 ▼
_00002.png

▼ Archiveが終了すると、下記の画面になるので、「Validate App」を押下 ▼
_00004.png

▼ そのまま「Next」を押下 ▼
_00005.png

▼ 「Automatically manage signing」を選択して「Next」を押下 ▼
_00006.png

▼ 「Validate」を押下 ▼
_00007.png

▼ 成功したので「Done」を押下 ▼
_00008.png

▼ Statusが「Validated」になっているので「Distribute App」を押下 ▼
_00009.png

▼ 「App Store Connect」を選択して「Next」を押下 ▼
_00010.png

▼ 「Upload」を選択して「Next」を押下 ▼
_00011.png

▼ そのまま「Next」を押下 ▼
_00012.png

▼ 「Automatically manage signing」を選択して「Next」を押下 ▼
_00013.png

▼ 「Upload」を押下 ▼
_00014.png

▼ アップロード中(3〜5分ぐらい) ▼
_00015.png

▼ 成功して「Done」を押下 ▼
_00016.png

⑧ 審査へ提出〜提出後の対応

  1. ビルドの追加
    アップロードが無事完了すると、20分〜30分ぐらいで、
    App Store Connect の 1.0 提出準備中 > ビルドの横に「+」ボタンが表示されるので、
    先ほどアップロードしたアプリを選択する。
    その際に輸出コンプラインスの回答をしなければならないが、暗号化をしないのであれば「いいえ」で
    暗号化をしているのであれば「はい」を選択して、アプリの内容に適した回答をしていきましょう。

  2. 審査へ提出
    1.0 提出準備中 の右上の「保存」ボタンの横に「審査へ提出」ボタンがあるので押下します。
    これでステータスが「審査待ち」になり、遅くとも3日ほどで審査が開始されます。

  3. 審査後の対応
    リジェクトされた内容が、アプリの修正を伴いような内容だった場合は、
    アプリを再ビルドして、先ほどのアップロード手順を再度行う必要がありますが、
    その際は、アプリのビルドバージョンを上げてアップロードする必要があります。
    ※ 筆者はビルドバージョンを1.0 → 1.0.1 → 1.0.2 のように上げています。
    _00017.png

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

iOSアプリ マルチデバイス対応をコピペでやるよ

マルチデバイス対応

端末によって画面サイズが異なるので端末に合わせた対応が必要。
その方法の1つをメモっておこ〜

SceneDelegate.swiftに以下をコピペしよう

SceneDelegate.swiftってファイルがデフォルトであるはずなので、そこのsceneメソッドのところに以下をコピペしましょ。
これをコピペしただけだとエラーが出るけど気にしないでおk。

//端末の画面の大きさを取得
let storyboard:UIStoryboard = self.grabStoryboard()
//表示するstoryBoardをセット!
if let window = window{
    window.rootViewController = storyboard.instantiateInitialViewController() as UIViewController?
    }
self.window?.makeKeyAndVisible()

次に、SceneDelegate.swiftの空いているところに以下をコピペしましょ。

//使用している端末は何かな?チェック!
func grabStoryboard() -> UIStoryboard{
            var storyboard = UIStoryboard()
            let height = UIScreen.main.bounds.size.height
            //画面の高さが667pxってことは、さてはiPhoneSE2ですかな??
            if height == 667 {
                storyboard = UIStoryboard(name: "iPhoneSE2", bundle: nil)
            //画面の高さが736pxってことは、さてはiPhone8plusですかな??
            }else if height == 736 {
                storyboard = UIStoryboard(name: "iPhone8plus", bundle: nil)
            //上のコメントと一緒な感じだよ。察してくれよな!
            }else if height == 812{
                storyboard = UIStoryboard(name: "iPhone11Pro", bundle: nil)
            //以下略
            }else if height == 896{
                storyboard = UIStoryboard(name: "iPhone11ProMax", bundle: nil)
            }else if height == 1112{ 
                storyboard = UIStoryboard(name: "iPad", bundle: nil)
            }else{
                switch UIDevice.current.model {
                case "iPnone" :
                storyboard = UIStoryboard(name: "se", bundle: nil)
                    break
                case "iPad" :
                storyboard = UIStoryboard(name: "iPad", bundle: nil)
                print("iPad")
                    break
                default:
                    break
                }
            }
            return storyboard
    }

storyboardを作るよ〜

次にstoryBordを対応する機種だけ作っておきます〜
新しいstoryBoardを作る → ファイル名を対応したい機種の名前に!(さっきコピペしたgrabStoryboardメソッドに示されている機種名だけ対応可)
そして、xcodeの下の方の「view as: iPhone SE(2nd generation)」とか書いてあるところをタップ

image.png

すると以下のような表示が出るから、ファイル名に指定した画面を選択

image.png

次に「Is initial controller」にチェックを入れる〜
スクリーンショット 2020-07-24 13.34.46.png

これを対応したい機種の種類分作成します。
これで完了!

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

3年くらい放置していたiOSアプリをビルドした時にやったこと。

はじめに

3年くらい放置していたアプリのメンテナンスが必要になり、ビルドをしたらぜんぜんダメだったのですが、解決方法を記録しておきます。
主にダメだったのは以下の点です

  • Swiftのバージョン
  • UITableView
  • UICollectionView

開発環境

  • 対応前 Xcode9
  • 対応後 Xcode11.5

対応内容

SWIFT_VERSIONエラー

エラ〜メッセージ
SWIFT_VERSION '3.0' is unsupported, supported versions are: 4.0, 4.2, 5.0.

もともとSwift3.0で書かれていたのですが、Xcode11ではすでにSwiftのバージョンは5.0になっています。しかしバージョン2つ飛ばしは心配なので、4.0を選択しました。
幸いにもSwift3.0から4.0の差分はそれほどないので、バージョンの変更だけですみました。
Build SettingsのSwift Language Versionを4.0に変更しました。

変更前

スクリーンショット 2020-07-23 18.04.27.png

変更後

スクリーンショット 2020-07-23 18.07.09.png

同様に、Podfileにもバージョン指定があったので、削除しました

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = '3.0'
    end
  end
end

UITableViewやUICollectionViewのDelegateが変更されていた

次に発生したのは以下のエラー

[xxxx.StoryViewController tableView:numberOfRowsInSection:]: unrecognized selector sent to instance 0x7fc48663aa50
[xxxx.StoryViewController tableView:numberOfRowsInSection:]: unrecognized selector sent to instance 0x7fc48663aa50'

UITabeViewDelegateとUITableViewDatasourceのメソッド名が変更されていました。
StoryBoardでDelegateをつなげるとProtocolを書かなくても問題なく動作していたのですが、代わりにコンパイル時にエラーチェックされないので、ちゃんとプロトコルを定義するように変更しました。
Protocolを追加しておけばXcodeの機能で自動的に変換できます。

修正前

class StoryViewController: UIViewController {
     :

修正後

class StoryViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
     :

Protocolを設定すると、以下のようにエラーメッセージが表示され、Fixボタンを押すと自動的に変換されます。
スクリーンショット 2020-07-24 9.08.56.png

最後に

分かってしまえば大したことのない修正項目ですが、これだけでも数時間費やしてしまいました。同じことにハマる人も多いと思いますので、(そんなにいないかな)念のため共有しておきます。
SwiftやiOS SDKは毎年変化があり、毎年メンテナンスしていればこのような問題はそれほど起きないのですが、バージョンを飛ばすと途端に対応が大変になったりしますね。

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