20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

初めてのオリジナルiPhoneアプリをつくるまで

Last updated at Posted at 2016-02-19

はじめてのおりじなるあぷり

※作成しながら書いてるので完成するまで編集中です
※はじめて結構ストック数上がってきてて需要あるんだなと思いつつビクビクしてます←
※New: ver.1.6.5で四度目ぐらいの提出をしています。その過程で色々弄ったので記事を最新版に更新しています。
※コードばっかで読みにくいとは思いますがご容赦ください。

作者のスキル

  • プログラミング自体の経験は5年ぐらい(趣味)
  • Androidアプリ作成経験あり
  • 主なコーディング経験は大体競技プログラミングで
  • iPhoneアプリ開発の基礎は学習した

最終目標

オリジナルのTwitterクライアントをつくる

過程やソースコード

Imagenius←是非使ってみてください!

この記事は?

作ってる最中に躓いたこととか、ちょっとしたTipsとかをまとめる
というわけでつらつら書いていきたいと思います。(随時更新します)

こっから下自由に語り

はじめてのリジェクト

ver.1.0.1として提出していたものがリジェクトされてしまいました。

理由としては

We noticed that several screens of your app were crowded or laid out in a way that made it difficult to use your app.

We’ve attached screenshot(s) for your reference. These screenshots represent some examples of this problem but may not represent all instances of this issue.

と。スクリーンショットを見てみるとTweet画面でプレースホルダー(入力部分が空の時に表示する文字)がないためにどこがどこなのかわからないというのが原因らしいです。
というわけでplaceholderを設定...といってもUITextViewにはPlaceHolderが無いそう。
結局ここを参考にTweetのViewControllerにTextViewDelegateを設定することで実現できました。

調べたらカスタムクラスをつくって...的なやり方もあったりするのですがこれが一番楽だと思います!
(結果はおって更新します)
2016/03/09 ツイート文章が空だとプレースホルダーの文字がつぶやかれてしまうバグが発生していました…審査を通ると画面遷移後に同じツイートが読み込まれてしまうバグとともにバグの残ったままリリースされることになりますが早急に修正する予定ですので利用していただける方はご了承ください。。。

2016/03/14 今度はMetadata rejectedとしてブロック・通報機能はあるか?みたいなことを聞かれ、無い、と返答したところ14.3項にもとづきEULAを書きなさいなどの指示がきました。ブロック・通報機能がないとは言ってもすべてTwitterの状態に依存しているのでどう書いていいかわからず今質問を出しています。
その過程でちょっと知らなかったことを調べてみたのですがビルド番号とバージョン番号というのがあるみたいですね。ビルド番号をずらしておけばバージョン番号同じままで再提出できるとか...(知らなかったせいで無駄なことをしてしまっていました汗)

2016/03/18 ブロック・通報機能をWebViewに丸投げしたら再ログインの必要があるからダメだよといわれました。おとなしく新しいボタンを実装しました。

TwitterSampleに関して

Fabric

Twitter純正のライブラリ。TweetのTableViewCellをTwitter社指定の形で出してくれるのが魅力。
ただしPOSTメソッドに関して結構厳しくてツイートはComposeViewというUIをいじれない形で、いいねはActioinButtonとしてやはり気軽にいじれず。特にこのActionButtonはいいねとシェアの2つでリツイートとかリプとかはない。

使い方はいろんなとこに書かれているから盲点になりやすいところだけ。

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // cellをTWTRTweetTableViewCellにキャストする
    let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! TWTRTweetTableViewCell
    if tweets.count > indexPath.row {
        cell.tweetView.delegate = self
        let tweet = tweets[indexPath.row]
        cell.tag = indexPath.row
        // これでアクションボタン表示可能だけどデフォルトのアクションボタン結構扱いにくい
        cell.tweetView.showActionButtons = true
        cell.configureWithTweet(tweet)
        if (tweets.count - 1) == indexPath.row && self.maxIdStr != "" {
            self.loadMore({() -> () in }, errcb: {() -> () in })
        }
    }
    return cell
}

まずはコメントどおりActionButtonを表示する方法

class TimelineViewController:BaseTweetViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        self.navigationItem.title = "タイムライン"
    }
    // max_idの管理が微妙なので余分に一個とっておいてそれをmaxIdStrに保存しておき次にはそこから読み込むようにする
    override func load(cb: () -> (), errcb: () -> ()) {
        let params = ["count": "41"]
        TwitterAPI.getHomeTimeLine(params, tweets: {
            twttrs in
            for var i = 0; i < twttrs.count-1; i++ {
                self.tweets.append(twttrs[i])
            }
            if tweets.count < 1 {
                self.maxId = ""
            } else if tweets.count == 1 {
                self.maxId = tweets[0]["id_str"].string
            } else {
                self.maxId = tweets[tweets.count - 1]["id_str"].string
            }
            self.tableView.reloadData()
            }, error: {
                error in
                // error handling
        })
    }
    override func loadMore(cb: () -> (), errcb: () -> ()) {
        let params = ["count": "41", "max_id": self.maxIdStr]
        TwitterAPI.getHomeTimeLine(params, tweets: {
            twttrs in
            for var i = 0; i < twttrs.count-1; i++ {
                self.tweets.append(twttrs[i])
            }
            if tweets.count < 1 {
                self.maxId = ""
            } else if tweets.count == 1 {
                self.maxId = tweets[0]["id_str"].string
            } else {
                self.maxId = tweets[tweets.count - 1]["id_str"].string
            }
            self.tableView.reloadData()
            }, error: {
                error in
                // error handling
        })
    }
}

maxIdは正しい取り方とかあんまり書いてないのでこのように処理した

Swifter

オープンソースのライブラリ、どうやらこっちは生でAPIとかを叩いているものをクラス化した感じのものらしい。ドキュメントが少ないのが微妙。。。
基本的なことならドキュメントにあり。
ちなみにTwitterSampleではまだ使ってない

使い方は英語の説明がいまいちアバウトでわかりにくいけど

  1. 入れたいプロジェクトを開いておく
  2. そこにダウンロードしたSwifty.xcodeprojを一番上の階層あたりにドラッグ&ドロップする
  3. ここ参考にframeworkをちゃんとプロジェクトに紐付けしておく(じゃないと実行時に急にエラーくる)

imageを投稿する時はNSData型なのでここを参考にPNG形式のNSDataに変更しましょう。

SwifterはAccountsのアカウントデータをつかって初期化できます。でもSegue変わってから同じものでログインとか面倒なので全部データとしていちいち渡しました。

// import文
import SwifteriOS
// 初期化はこんな感じでやります。OAuthを使うことも可能です。
class Hoge {
    var swifter: Swifter!
    init() {
        let accountStore = ACAccountStore()
        let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)
        let twitterAccounts = accountStore.accountsWithAccountType(accountType)
        let twitterAccount = twitterAccounts[0] as ! ACAccount
        swifter = Swifter(account: twitterAccount)
    }

// 以下はクラスの形で書くの面倒なので
// ホームタイムラインを取得するもの
swifter.getStatusesHomeTimelineWithCount(41, sinceID: nil, maxID: self.maxId, trimUser: nil, contributorDetails: nil, includeEntities: nil, success: successHandler, failure: failureHandler)
// 同じ引数でgetStatusesMentionTimelineWithCountにすると自分へのリプライが取得できます。
// successHandlerやfailureHandlerはこんな感じにすると良いと思います。
let failureHandler: ((NSError) -> Void) = { error in
    Utility.simpleAlert(String(error.localizedFailureReason), presentView: self)
    }
let successHandler: (([JSONValue]?) -> Void) = { statuses in
    guard let tweets = statuses else { return }
    self.tweetArray = []
    for var i = 0; i < tweets.count - 1; i++ {
        self.tweetArray.append(tweets[i])
    }
    if tweets.count < 1 {
        self.maxId = ""
    } else if tweets.count == 1 {
        self.maxId = tweets[0]["id_str"].string
    } else {
        self.maxId = tweets[tweets.count - 1]["id_str"].string
    }
    self.timelineTableView.reloadData()
}

※変更(2016/02/25):ツイートが無い人とか一個しかない人とかのことを全然考えてなかったので条件分岐追加

SwifterはFabricみたいにきちんと設計されたTweetTableViewCellを用意してくれるわけではないので当然リンクなどの装飾は全くありません。さらにAutoLayoutでのTableViewCellの高さ自動変更などやりたい場合はUILabelが一番楽です。よって以下の参考サイトにあるUILabelの拡張ライブラリを導入しました。

サイト2にブリッジングヘッダーについて書いてありますが実はCocoapodsで提供されるxcworkspace上で作業している分にはブリッジングヘッダーはそもそも必要ないようです(ちゃんと動作確認もとれました。)

ついでに、AutoLayoutの時は特に下のマージンを設定する時とか何も考えずにやってると2個以上の要素があるとどっちかが優先されて片方が見きれるみたいなことが起きてしまいますがそういう時はRelationsという項目をGreater Than or Equalとかにしておくと自動でどっちにあわせるかとかやってくれるので便利です。

どうやらSwifterだと返ってくる結果にMedia情報が公式APIの形で含まれていないらしい...?→extended_entitiesにあります。

リプライに関してですが

swifter.postStatusUpdate(tweetText!, media: UIImagePNGRepresentation(tweetImage!)!, inReplyToStatusID: replyID)

このようにinReplyToStatusIDにリプライの対象のTweetのIDを放り込めばリプライとなります。一応@ホニャララも自動でつけてあげるようにしましょう。そこら辺はtweetを扱ってるクラスで適当に。

TwitterUtil

それでSwifterを使うにあたって絶対にどのViewControllerでも使うログイン周りの処理はクラスにまとめました。

import Foundation
import UIKit
import Accounts
import SwifteriOS

class TwitterUtil {
    // login
    class func loginTwitter(present: UIViewController, success: ((ACAccount?) -> ())? = nil) {
        let accountStore = ACAccountStore()
        var accounts = [ACAccount]()
        let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)
        accountStore.requestAccessToAccountsWithType(accountType, options: nil) { granted, error in
            if granted {
                accounts = accountStore.accountsWithAccountType(accountType) as! [ACAccount]
                if accounts.count == 0 {
                    Utility.simpleAlert("Error: Twitterアカウントを設定してください。", presentView: present)
                } else {
                    self.showAndSelectTwitterAccountWithSelectionSheets(accounts, present: present, success: success)
                }
            } else {
                Utility.simpleAlert("Error", presentView: present)
            }
        }
    }
    
    // Twitterアカウントの切り替え
    class func showAndSelectTwitterAccountWithSelectionSheets(accounts: [ACAccount], present: UIViewController, success: ((ACAccount?)->())? = nil) {
        // アクションシートの設定
        let alertController = UIAlertController(title: "アカウント選択", message: "使用するTwitterアカウントを選択してください", preferredStyle: .ActionSheet)
        let saveData: NSUserDefaults = NSUserDefaults.standardUserDefaults()
        
        for var i=0; i<accounts.count; i++ {
            let account = accounts[i]
            alertController.addAction(UIAlertAction(title: account.username, style: .Default, handler: { (action) -> Void in
                // 選択したアカウントを返す
                for var j=0; j<accounts.count; j++ {
                    if account == accounts[j] {
                        print(j)
                        saveData.setObject(j, forKey: Settings.Saveword.twitter)
                        break
                    }
                }
                success?(account)
            }))
            
        }
        
        // キャンセルボタンは何もせずにアクションシートを閉じる
        let CanceledAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
        alertController.addAction(CanceledAction)
        
        // アクションシート表示
        present.presentViewController(alertController, animated: true, completion: nil)
    }
    
    // 画像がツイートに含まれているか?
    class func isContainMedia(tweet: JSONValue) -> Bool {
        if tweet["extended_entities"].object != nil {
            return true
        }
        return false
    }
}

successというSwifterのsuccessHandlerと似たようなことをしてるのはログインあたりの処理とかが非同期通信をつかっているみたいなのでこの関数の中でログインした後にしなきゃならないこととかを記述しておかないとViewが読み込まれた後にようやくログインされて〜みたいな予想外の挙動になってしまうためです。

あとNSUserDefaultsがsegueを貼り直してから大活躍しているのですが(というのも戻るときにデータを受け渡せないから)ACAccountはそのまま保存するとエラーがおきます。よってAccountsStoreの何番目に入っているのかという情報を保存することにしました。

ところでadd Exception Breakpointをallにしてると毎回このアクションシート表示のところでブレークポイント打たれるんだけど今のところそこでエラー起きたこと無いしなんなのかなぁと思ってます

Twitter用のカスタムセル

リンクの設定とかしたので参考程度に
AutoLayoutを上手く使うことで画像のある時と無い時どちらでも再利用可能にしています。

ハッシュタグや@は正規表現を使ってリンクを追加していますね。特にハッシュタグのURLを作る時は日本語が混ざったりしてる場合もあるのでちゃんと変換してあげましょう。正規表現は色々頑張ってますが、RegExCategoryというライブラリを使うともっと簡潔に書ける可能性もあります。これは内部WebViewのところで使っています。

import UIKit
import SwifteriOS
import TTTAttributedLabel
import SWTableViewCell

class TweetVarViewCell: SWTableViewCell {
    @IBOutlet var tweetLabel: TTTAttributedLabel!
    @IBOutlet var userIDLabel: UILabel!
    @IBOutlet var userLabel: UILabel!
    @IBOutlet var userImgView: UIImageView!
    @IBOutlet var tweetImgView: UIImageView!
    @IBOutlet var tweetSubView: UIView!
    @IBOutlet var subViewHeight: NSLayoutConstraint!
    @IBOutlet var imageCountLabel: UILabel!
    
    // TableViewCellが生成された時------------------------------------------------
    override func awakeFromNib() {
        super.awakeFromNib()
        self.tweetLabel.enabledTextCheckingTypes = NSTextCheckingType.Link.rawValue
        self.tweetLabel.extendsLinkTouchArea = false
        self.tweetLabel.linkAttributes = [
            kCTForegroundColorAttributeName: Settings.Colors.twitterColor,
            NSUnderlineStyleAttributeName: NSNumber(long: NSUnderlineStyle.StyleNone.rawValue)
        ]
        subViewHeight.constant = 0
        self.tweetImgView.userInteractionEnabled = true
    }
    
    // TTTAttributedLabel関連----------------------------------------------------
    // mention link
    func highrightMentionsInLabel(label: TTTAttributedLabel) {
        let text: NSString = label.text!
        let mentionExpression = try? NSRegularExpression(pattern: "(?<=^|\\s)(@\\w+)", options: [])
        let matches = mentionExpression!.matchesInString(label.text!, options: [], range: NSMakeRange(0, text.length))
        for match in matches {
            let matchRange = match.rangeAtIndex(1)
            let mentionString = text.substringWithRange(matchRange)
            let user = mentionString.substringFromIndex(mentionString.startIndex.advancedBy(1))
            let linkURLString = NSString(format: "https://twitter.com/%@", user)
            label.addLinkToURL(NSURL(string: linkURLString as String), withRange: matchRange)
        }
    }
    // hashtag link
    func highrightHashtagsInLabel(label: TTTAttributedLabel) {
        let text: NSString = label.text!
        let mentionExpression = try? NSRegularExpression(pattern: "(?<=^|\\s)(#\\w+)", options: [])
        let matches = mentionExpression!.matchesInString(label.text!, options: [], range: NSMakeRange(0, text.length))
        for match in matches {
            let matchRange = match.rangeAtIndex(0)
            let hashtagString = text.substringWithRange(matchRange)
            let word = hashtagString.substringFromIndex(hashtagString.startIndex.advancedBy(1))
            let linkURLString = NSString(format: "https://twitter.com/hashtag/%@", word.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)
            label.addLinkToURL(NSURL(string: linkURLString as String), withRange: matchRange)
        }
    }
    
    // 要素の設定-----------------------------------------------------------------
    func setOutlet(tweet: JSONValue, tweetHeight: CGFloat) {
        let userInfo = tweet["user"]
        
        self.tweetLabel.text = Utility.convertSpecialCharacters(tweet["text"].string!)
        self.highrightHashtagsInLabel(tweetLabel)
        self.highrightMentionsInLabel(tweetLabel)
        
        self.userLabel.text = userInfo["name"].string
        let userID = userInfo["screen_name"].string!
        self.userIDLabel.text = "@\(userID)"
        let userImgPath:String = userInfo["profile_image_url_https"].string!
        let userImgURL:NSURL = NSURL(string: userImgPath)!
        let userImgPathData:NSData? = NSData(contentsOfURL: userImgURL)
        if userImgPathData != nil {
            self.userImgView.image = UIImage(data: userImgPathData!)
        } else {
            self.userImgView.image = UIImage(named: "user_empty")
        }
        self.userImgView.layer.cornerRadius = self.userImgView.frame.size.width * 0.5
        self.userImgView.clipsToBounds = true
        
        // こっから下で画像の枚数とそれに応じたレイアウトを行う
        guard let tweetMedia = tweet["extended_entities"]["media"].array else {
            subViewHeight.constant = 0
            self.layoutIfNeeded()
            return
        }
        
        let imageCount = tweetMedia.count
        subViewHeight.constant = tweetHeight
        tweetSubView.layer.cornerRadius = tweetSubView.frame.width * 0.017
        tweetSubView.clipsToBounds = true
        tweetSubView.layer.borderColor = Settings.Colors.selectedColor.CGColor
        tweetSubView.layer.borderWidth = 0.19
        let tweetImgPath:String = tweet["extended_entities"]["media"][0]["media_url"].string!
        let tweetImgURL:NSURL = NSURL(string: tweetImgPath)!
        let tweetImgPathData:NSData = NSData(contentsOfURL: tweetImgURL)!
        self.tweetImgView.image = UIImage(data: tweetImgPathData)!
        switch tweet["extended_entities"]["media"][0]["type"].string! {
        case "photo":
            imageCountLabel.text = "\(imageCount)枚の写真"
        case "video":
            imageCountLabel.text = "動画"
        default:
            imageCountLabel.text = "GIF"
        }
        self.layoutIfNeeded()
    }
    
    // Utility------------------------------------------------------------------
}

TwitterAPIについて

なんか色々ところどころ編集していってるのでどのセクションになにがあるのか、統一感がなくなってきてますが。。。

TwitterAPIに関して、REST API 1.1はGETの時にinclude_entitiesとするとURL情報とかそういうのを取得できるわけですが、今ネットにある情報だけ見るとentitiesのmediaという項目を見るとmediaの情報が入っているように思われてしまいます。
ですが、実は現在のTwitterからのレスポンスにはentitiesにmediaという項目がなくなっていてかわりにextended_entitiesにmediaの情報が入ってるようです。そしてこれは複数のmedia情報に対応しているようなのでJSONの配列になっています。

let tweetImgPath:String = tweet["extended_entities"]["media"][0]["media_url"].string!

このようにすることで画像のURLを取得できました。ちなみに"media_url"とするとこのメディアがビデオでもgifでもサムネイルの画像URLを取得できるので面倒な条件分岐が必要なくなり便利です。

ImageCollectionViewについて

AlamofireとSwiftyJSON

おおよそこんなことしてます。
ライブラリの導入はCarthageを使いました。Podsでも多分出来ますがCarthageの使い方の説明でこの2つのライブラリよく使われているのでこっちのほうがやり方すぐにわかると思います。

import UIKit
import Alamofire
import SwiftyJSON
import DZNEmptyDataSet
import KTCenterFlowLayout

class ImageViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, DZNEmptyDataSetDelegate, DZNEmptyDataSetSource {
    @IBOutlet var imageCollectionView: UICollectionView!
    
    var searchWord: String = ""
    var reqs: [NSURLRequest] = []
    var selectedImage: UIImage?
    var imageSize: CGFloat!
    
    let saveData:NSUserDefaults = NSUserDefaults.standardUserDefaults()
    
    // UIViewControllerの設定----------------------------------------------------
    override func viewDidLoad() {
        super.viewDidLoad()
        imageCollectionView.dataSource = self
        imageCollectionView.delegate = self
        
        if saveData.objectForKey(Settings.Saveword.search) != nil {
            searchWord = saveData.objectForKey(Settings.Saveword.search) as! String
        }
        
        // AutoLayout対応のためセル調整
        imageSize = (self.view.frame.width) / 4
        let flowLayout = KTCenterFlowLayout()
        flowLayout.scrollDirection = .Vertical
        flowLayout.minimumInteritemSpacing = 0
        flowLayout.minimumLineSpacing = 0
        flowLayout.itemSize = CGSizeMake(imageSize, imageSize)
        imageCollectionView.collectionViewLayout = flowLayout
        
        self.imageCollectionView.emptyDataSetDelegate = self
        self.imageCollectionView.emptyDataSetSource = self
        
        tiqav()
    }
    
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "toResultView" {
            let resultView = segue.destinationViewController as! ResultViewController
            resultView.image = self.selectedImage
        }
    }
    
    override func preferredStatusBarStyle() -> UIStatusBarStyle {
        return UIStatusBarStyle.LightContent
    }
    
    
    // ボタン関連-----------------------------------------------------------------
    // キャンセルのボタン
    @IBAction func cancelButton() {
        dismissViewControllerAnimated(true, completion: nil)
    }
    
    
    // CollectionViewまわりの設定-------------------------------------------------
    // セクション数
    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
    // 画像の数
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return reqs.count
    }
    // 入れるもの
    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell: ImageViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("imageCell", forIndexPath: indexPath) as! ImageViewCell
        let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
        let  task = session.dataTaskWithRequest(reqs[indexPath.row]){(data, res, err)->Void in
            dispatch_async(dispatch_get_main_queue(), {
                let image = UIImage(data: data!)
                cell.imageView.image = Utility.cropThumbnailImage(image!, w: Int(self.imageSize), h: Int(self.imageSize))
            })
        }
        task.resume()
        return cell
    }
    // 画像を選択したら
    func  collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
        let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
        let  task = session.dataTaskWithRequest(reqs[indexPath.row], completionHandler: {(data, res, err)->Void in
            self.selectedImage = UIImage(data: data!)
            dispatch_async(dispatch_get_main_queue(), {
                self.performSegueWithIdentifier("toResultView", sender: nil)
            })
        })
        task.resume()
    }
    // DZNEmptyDataSetの設定
    func descriptionForEmptyDataSet(scrollView: UIScrollView!) -> NSAttributedString! {
        let text = "該当する画像が見つかりませんでした。"
        let font = UIFont.systemFontOfSize(20)
        return NSAttributedString(string: text, attributes: [NSFontAttributeName: font])
    }
    
    // Utility------------------------------------------------------------------
    // Tiqav.comでの検索
    func tiqav() {
        let text = "http://api.tiqav.com/search.json?q="+searchWord
        Alamofire.request(.GET, encodeURL(text), parameters: nil).responseJSON(completionHandler: {
            response in
            guard let object = response.result.value else {
                return
            }
            let json = SwiftyJSON.JSON(object)
            json.forEach({(_, json) in
                let url = NSURL(string: "http://img.tiqav.com/" + String(json["id"]) + "." + json["ext"].string!)
                let req = NSURLRequest(URL: url!)
                self.reqs.append(req)
            })
            // CollectionViewをリロードする
            self.imageCollectionView.reloadData()
        })
    }
    // 日本語を含む検索語でAPIを叩くため
    func encodeURL(text: String) -> NSURL! {
        return NSURL(string: text.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)!
    }
}

こんな感じで。
※変更(2016/02/21):画像の読み出しに非同期通信をつかっているのでSegue呼び出しの前にちゃんと画像を読み込んだ
※変更(2016/02/25):segueで戻るmodalは非推奨とのことでNavigationControllerとか使ってsegue構築しなおしたためcancelをdismissで
※変更(2016/03/19):最新版で非推奨になっていた非同期通信まわりの関数とか文字列の変換関数などをちゃんとしたものに書き換えています。

ちなみにResultViewからImageViewに戻るときはこう

// Cancelボタンのとき
    @IBAction func pushCancel() {
        self.navigationController?.popToRootViewControllerAnimated(true)
    }

segueはpushが次の画面、modalが新しい画面みたいなこと詳しく書いたページもあるんだけども個人のgithubリポジトリのissueなのでリンクは貼らないでおきます。
→実は最新版ではpushやmodalも非推奨になっていてかわりにshowやpresent modallyを使うといいのですが、詳しい対応関係などはQiitaにもあるので僕のストックから漁ったり適当に探したりしてみてください。

あと非推奨とのことですがアプリの仕様上さけられないのでUIImageをNSUserDefaultsに保存するために、そのままでは出来ないので下のようなことをやってます(調べると情報沢山出てきますが)

let imageData:NSData = UIImagePNGRepresentation(image!)!
saveData.setObject(imageData, forKey: Settings.Saveword.image)
if saveData.objectForKey(Settings.Saveword.search) != nil {
    saveData.setObject(nil, forKey: Settings.Saveword.search)
}

TableView関連のライブラリについて

DZNEmptyDataSet

TableViewの要素がないときに表示する。結構簡単。ここここを参考にすると使い方はわかると思う。インストールはCocoapodsで。

SWTableViewCell

スライドするとボタン出してくれるみたいな。使い方はこことかこことか。

でも全部Objective-Cの情報なので苦労しましたが...
まずオリジナルのCellを使いたくてUITableViewCellを継承していたクラスを書いてたけどそれをすべてSWTableViewCellに書き換えることでCellの準備は完了(もちろんimportは必要)

スワイプして出てきたボタンをおした時の挙動を記述したいclassの継承クラスにSWTableViewCellDelegateを追加する。あとは下みたいに書く(今回はSwifterの処理をしたかったので)

class MainViewController: SWTableViewCellDelegate, ... {

...
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        if tweetArray.count <= indexPath.row || indexPath.row < 0 {
            // Utility.simpleAlert("ローディング中にエラーが発生しました。", presentView: self)
            return UITableViewCell()
        }
        let tweet = tweetArray[indexPath.row]
        let favorited = tweet["favorited"].bool!
        let retweeted = tweet["retweeted"].bool!
        
        let cell: TweetVarViewCell = tableView.dequeueReusableCellWithIdentifier("TweetCellPrototype") as! TweetVarViewCell
        cell.tweetLabel.delegate = self
        cell.setOutlet(tweet, tweetHeight: self.view.bounds.width / 1.8)
        
        let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tapped:")
        cell.tweetImgView.addGestureRecognizer(tapGesture)
        cell.tweetImgView.tag = indexPath.row
        
        if (self.tweetArray.count - 1) == indexPath.row && self.maxId != "" {
            self.loadMore()
        }
        cell.rightUtilityButtons = self.rightButtons(favorited, retweeted: retweeted) as [AnyObject]
        cell.leftUtilityButtons = self.leftButtons() as [AnyObject]
        cell.delegate = self
        cell.layoutIfNeeded()
        return cell
    }
    // imageViewがタップされたら画像のURLを開く
    func tapped(sender: UITapGestureRecognizer) {
        if let theView = sender.view {
            let rowNum = theView.tag
            if tweetArray[rowNum]["extended_entities"]["media"][0]["type"].string == nil {
                
            }
            switch tweetArray[rowNum]["extended_entities"]["media"][0]["type"].string! {
            case "photo":
                let tempData = NSMutableArray()
                for data in tweetArray[rowNum]["extended_entities"]["media"].array! {
                    tempData.addObject(NSData(contentsOfURL: NSURL(string: data["media_url"].string!)!)!)
                }
                imageData = tempData
                performSegueWithIdentifier("toPreView", sender: nil)
            default:
                if let imagURL = tweetArray[rowNum]["extended_entities"]["media"][0]["url"].string {
                    Utility.openWebView(NSURL(string: imagURL)!)
                    performSegueWithIdentifier("openWebView", sender: nil)
                }
            }
        }
    }
    // SWTableViewCell関連
    // 右のボタン
    func rightButtons(favorited: Bool, retweeted: Bool) -> NSArray {
        let rightUtilityButtons: NSMutableArray = NSMutableArray()
        if favorited {
            rightUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.favColor, icon: (UIImage(named: "like-action")!)))
        } else {
            rightUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.selectedColor, icon: UIImage(named: "like-action")!))
        }
        rightUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.twitterColor, icon: UIImage(named: "reply-action_0")!))
        if retweeted {
            rightUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.retweetColor, icon: UIImage(named: "retweet-action")!))
        } else {
            rightUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.selectedColor, icon: UIImage(named: "retweet-action")!))
        }
        rightUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.deleteColor, icon: UIImage(named: "caution")!))
        return rightUtilityButtons
    }
    // 左のボタン
    func leftButtons() -> NSArray {
        let leftUtilityButtons: NSMutableArray = NSMutableArray()
        leftUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.twitterColor, icon: UIImage(named: "TwitterLogo_white_1")!))
        leftUtilityButtons.addObject(addUtilityButtonWithColor(Settings.Colors.userColor, icon: UIImage(named: "use_white")!))
        return leftUtilityButtons
    }
    // ボタンの追加(なんかObj-CのNSMutableArray拡張ヘッダーが上手く反映できてないので)
    func addUtilityButtonWithColor(color : UIColor, icon : UIImage) -> UIButton {
        let button:UIButton = UIButton(type: UIButtonType.Custom)
        button.backgroundColor = color
        button.setImage(icon, forState: .Normal)
        return button
    }
    // 右スライドした時のボタンの挙動
    func swipeableTableViewCell(cell: SWTableViewCell!, didTriggerRightUtilityButtonWithIndex index: Int) {
        let cellIndexPath: NSIndexPath = self.timelineTableView.indexPathForCell(cell)!
        let tweet = tweetArray[cellIndexPath.row]
        switch index {
        case 0:
            // fav
            if tweet["favorited"].bool! {
                swifter.postDestroyFavoriteWithID(tweet["id_str"].string!, success: {
                    statuses in
                    (cell.rightUtilityButtons[0] as! UIButton).backgroundColor = Settings.Colors.selectedColor
                })
                break
            }
            swifter.postCreateFavoriteWithID(tweet["id_str"].string!, success: {
                statuses in
                (cell.rightUtilityButtons[0] as! UIButton).backgroundColor = Settings.Colors.favColor
            })
            break
        case 1:
            // reply
            replyID = tweet["id_str"].string
            replyStr = "@\(tweet["user"]["screen_name"].string!) "
            performSegueWithIdentifier("toTweetView", sender: nil)
            break
        case 2:
            // retweet
            if tweet["retweeted"].bool! {
                // (cell.rightUtilityButtons[2] as! UIButton).backgroundColor = Settings.Colors.selectedColor
                break
            }
            swifter.postStatusRetweetWithID(tweet["id_str"].string!, success: {
                statuses in
                (cell.rightUtilityButtons[2] as! UIButton).backgroundColor = Settings.Colors.retweetColor
            })
            break
        case 3:
            // block or spam
            let failureHandler: ((NSError) -> Void) = { error in
                Utility.simpleAlert(String(error.localizedFailureReason), presentView: self)
            }
            let successHandler: ((user: Dictionary<String, JSONValue>?) -> Void) = { statuses in
                self.tweetArray = []
                self.loadTweet()
            }
            let screen_name = tweet["user"]["screen_name"].string!
            let alertController = UIAlertController(title: "ブロック・通報", message: "@\(screen_name)を", preferredStyle: .ActionSheet)
            alertController.addAction(UIAlertAction(title: "ブロックする", style: .Default, handler: {(action)->Void in
                let otherAlert = UIAlertController(title: "\(screen_name)をブロックする", message: "本当にブロックしますか?", preferredStyle: .Alert)
                otherAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: {(action)->Void in
                    self.swifter.postBlocksCreateWithScreenName(screen_name, includeEntities: true, success: successHandler, failure: failureHandler)
                }))
                otherAlert.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
                self.presentViewController(otherAlert, animated: true, completion: nil)
            }))
            alertController.addAction(UIAlertAction(title: "通報する", style: .Default, handler: {(action)->Void in
                let otherAlert = UIAlertController(title: "\(screen_name)を通報する", message: "本当に通報しますか?", preferredStyle: .Alert)
                otherAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: {(action)->Void in
                    self.swifter.postUsersReportSpamWithScreenName(screen_name, success: successHandler, failure: failureHandler)
                }))
                otherAlert.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
                self.presentViewController(otherAlert, animated: true, completion: nil)
            }))
            alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
            self.presentViewController(alertController, animated: true, completion: nil)
        default:
            break
        }
    }
    // 左スライドした時のボタンの挙動
    func swipeableTableViewCell(cell: SWTableViewCell!, didTriggerLeftUtilityButtonWithIndex index: Int) {
        let cellIndexPath: NSIndexPath = self.timelineTableView.indexPathForCell(cell)!
        let tweet = tweetArray[cellIndexPath.row]
        switch index {
        case 0:
            let url = NSURL(string: "https://twitter.com/"+tweet["user"]["screen_name"].string!+"/status/"+tweet["id_str"].string!)!
            Utility.openWebView(url)
            performSegueWithIdentifier("openWebView", sender: nil)
            break
        case 1:
            let url = NSURL(string: "https://twitter.com/"+tweet["user"]["screen_name"].string!)!
            Utility.openWebView(url)
            performSegueWithIdentifier("openWebView", sender: nil)
        default:
            break
        }
    }
...

}

リプライに関しては別途上に書いておきます。

いいねやリツイートをした後に色が変わらないと面倒なのでどちらもリクエストが成功した後に色を変更するように設定しました。

タッチイベントなどは適宜MainViewControllerでとっているのですが、アイコンぐらい小さい画像のタップ判定や選択不可にしたセルの長押し判定など一部上手く反応してくれない場合もあるようです。

TTTAttributedLabel

下のリンクにも色々あるんだけどここを参考にしつつ数多のQiita記事も見ながらメンションとハッシュタグにリンクを貼る関数をかいてみました。
ちなみにハッシュタグの方の正規表現はこっちがいいかも?

※コードは上にまとめてあります。

WebViewについて

以下のようにWKWebViewを利用しました。ドキュメントが少なくて苦労しましたがこれで一応最低限の機能が実装されています。observerというもので状態を判断しているのですがそこでリロードとストップボタンが入れ替わるようにしてみました。

import UIKit
import WebKit

class WebViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
    @IBOutlet var backButton: UIBarButtonItem!
    @IBOutlet var forwardButton: UIBarButtonItem!
    
    var reloadButton: UIBarButtonItem!
    var stopButton: UIBarButtonItem!
    var webView: WKWebView!
    var url: NSURL = NSURL(string: "https://twitter.com")!
    
    let saveData:NSUserDefaults = NSUserDefaults.standardUserDefaults()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        self.webView.addObserver(self, forKeyPath: "title", options: NSKeyValueObservingOptions.New, context: nil)
        self.webView.addObserver(self, forKeyPath: "loading", options: NSKeyValueObservingOptions.New, context: nil)
        self.webView.addObserver(self, forKeyPath: "canGoBack", options: NSKeyValueObservingOptions.New, context: nil)
        self.webView.addObserver(self, forKeyPath: "canGoForward", options: NSKeyValueObservingOptions.New, context: nil)
        
        if saveData.objectForKey(Settings.Saveword.url) != nil {
            url = NSURL(string: saveData.objectForKey(Settings.Saveword.url) as! String)!
        }
        
        let request = NSURLRequest(URL: url)
        self.webView.loadRequest(request)
        reloadButton = UIBarButtonItem(barButtonSystemItem: .Refresh, target: self, action: "didTapReloadButton:")
        stopButton = UIBarButtonItem(barButtonSystemItem: .Stop, target: self, action: "didTapStopButton:")
        self.navigationItem.rightBarButtonItem = reloadButton
    }
    // WKWebViewの設定
    override func loadView() {
        super.loadView()
        self.webView = WKWebView()
        self.webView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([NSLayoutConstraint(item: self.webView, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Width, multiplier: 1.0, constant: 0),
            NSLayoutConstraint(item: self.webView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Height, multiplier: 1.0, constant: 0)])
        self.webView.navigationDelegate = self
        self.webView.UIDelegate = self
        self.webView.allowsBackForwardNavigationGestures = true
        self.view.insertSubview(self.webView, atIndex: 0)
    }
    deinit {
        self.webView.removeObserver(self, forKeyPath: "title")
        self.webView.removeObserver(self, forKeyPath: "loading")
        self.webView.removeObserver(self, forKeyPath: "canGoBack")
        self.webView.removeObserver(self, forKeyPath: "canGoForward")
    }
    // 状態に応じて
    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if keyPath == "title" {
            self.title = self.webView.title
        } else if keyPath == "loading" {
            UIApplication.sharedApplication().networkActivityIndicatorVisible = self.webView.loading
            if self.webView.loading {
                self.navigationItem.rightBarButtonItem = stopButton
            } else {
                self.navigationItem.rightBarButtonItem = reloadButton
            }
        } else if keyPath == "canGoBack" {
            self.backButton.enabled = self.webView.canGoBack
        } else if keyPath == "canGoForward" {
            self.forwardButton.enabled = self.webView.canGoForward
        }
    }
    
    override func preferredStatusBarStyle() -> UIStatusBarStyle {
        return UIStatusBarStyle.LightContent
    }
    
    // ボタン関連-----------------------------------------------------------------
    @IBAction func didTapBackButton() {
        self.webView.goBack()
    }
    @IBAction func didTapForwardButton() {
        self.webView.goForward()
    }
    @IBAction func safariButton() {
        let url = self.webView.URL
        Utility.shareSome(url!, text: self.webView.title, presentView: self)
    }
    internal func didTapReloadButton(sender: AnyObject) {
        self.webView.reload()
    }
    internal func didTapStopButton(sender: AnyObject) {
        self.webView.stopLoading()
    }
    
    // WKWebView関連-------------------------------------------------------------
    func webView(webView: WKWebView, createWebViewWithConfiguration configuration: WKWebViewConfiguration, forNavigationAction navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
        if navigationAction.targetFrame == nil {
            webView.loadRequest(navigationAction.request)
        }
        return nil
    }
    func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
        let currentUrl = navigationAction.request.URL
        let urlString = currentUrl?.absoluteString
        if NSRegularExpression.rx("\\/\\/itunes\\.apple\\.com\\/").isMatch(urlString) {
            UIApplication.sharedApplication().openURL(currentUrl!)
            decisionHandler(WKNavigationActionPolicy.Cancel)
            return
        } else if !NSRegularExpression.rx("^https?:\\/\\/.", ignoreCase: true).isMatch(urlString) {
            UIApplication.sharedApplication().openURL(currentUrl!)
            decisionHandler(WKNavigationActionPolicy.Cancel)
            return
        }
        decisionHandler(WKNavigationActionPolicy.Allow)
    }
}

その他Tips

単純なアラートを作る関数は度々使うのでこんな感じにどっからでも使える感じをめざした。画像Cropもここから。

import Foundation
import UIKit
import Accounts

class Utility {
    // UI関連--------------------------------------------------------------------
    // 単純なアラートをつくる関数
    class func simpleAlert(titleString: String, presentView: UIViewController) {
        let alertController = UIAlertController(title: titleString, message: nil, preferredStyle: .Alert)
        let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
        alertController.addAction(defaultAction)
        presentView.presentViewController(alertController, animated: true, completion: nil)
    }
    // Safariで開く
    class func openWebView(url: NSURL) {
        let saveData:NSUserDefaults = NSUserDefaults.standardUserDefaults()
        saveData.setObject(url.absoluteString, forKey: Settings.Saveword.url)
    }
    // share
    class func shareSome(url: NSURL, text: String? = nil, presentView: UIViewController) {
        let activityItems: [AnyObject]!
        if text != nil {
            activityItems = [url, text!]
        } else {
            activityItems = [url]
        }
        let activityVC = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
        let excludedActivityTypes = [UIActivityTypePostToWeibo, UIActivityTypePostToTencentWeibo]
        activityVC.excludedActivityTypes = excludedActivityTypes
        presentView.presentViewController(activityVC, animated: true, completion: nil)
    }
    
    // 画像処理------------------------------------------------------------------
    // 画像をあらかじめクロップしておく
    class func cropThumbnailImage(image :UIImage, w:Int, h:Int) -> UIImage {
        let origRef    = image.CGImage;
        let origWidth  = Int(CGImageGetWidth(origRef))
        let origHeight = Int(CGImageGetHeight(origRef))
        var resizeWidth:Int = 0, resizeHeight:Int = 0
        
        if (origWidth < origHeight) {
            resizeWidth = w
            resizeHeight = origHeight * resizeWidth / origWidth
        } else {
            resizeHeight = h
            resizeWidth = origWidth * resizeHeight / origHeight
        }
        
        let resizeSize = CGSizeMake(CGFloat(resizeWidth), CGFloat(resizeHeight))
        UIGraphicsBeginImageContext(resizeSize)
        
        image.drawInRect(CGRectMake(0, 0, CGFloat(resizeWidth), CGFloat(resizeHeight)))
        
        let resizeImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        // 切り抜き処理
        
        let cropRect  = CGRectMake(
            CGFloat((resizeWidth - w) / 2),
            CGFloat((resizeHeight - h) / 2),
            CGFloat(w), CGFloat(h))
        let cropRef   = CGImageCreateWithImageInRect(resizeImage.CGImage, cropRect)
        let cropImage = UIImage(CGImage: cropRef!)
        
        return cropImage
    }
    // 画像のリサイズ
    class func resizeImage(image: UIImage, size: CGSize) -> UIImage {
        let widthRatio = size.width / image.size.width
        let heightRatio = size.height / image.size.height
        let ratio = (widthRatio < heightRatio) ? widthRatio : heightRatio
        let resizedSize = CGSize(width: (image.size.width * ratio), height: (image.size.height * ratio))
        UIGraphicsBeginImageContext(resizedSize)
        image.drawInRect(CGRect(x: 0, y: 0, width: resizedSize.width, height: resizedSize.height))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resizedImage
    }
    
    // その他--------------------------------------------------------------------
    // HTML特殊文字を変換する
    // https://gist.github.com/mikesteele/70ae98d04fdc35cb1d5f
    class func convertSpecialCharacters(string: String) -> String {
        var newString = string
        let char_dictionary = [
            "&amp;": "&",
            "&lt;": "<",
            "&gt;": ">",
            "&quot;": "\"",
            "&apos;": "'"
        ];
        for (escaped_char, unescaped_char) in char_dictionary {
            newString = newString.stringByReplacingOccurrencesOfString(escaped_char, withString: unescaped_char, options: NSStringCompareOptions.RegularExpressionSearch, range: nil)
        }
        return newString
    }
}

あとは最近行った勉強会でenumの活用法みたいなことを習ったのでSettings.swiftとしてこんなファイルを用意しています。

import UIKit

enum Settings {
    enum Colors {
        // static letにすると一度だけしか生成されない
        static let mainColor = UIColor()
        static let favColor = UIColor(red: 232/255, green: 28/255, blue: 79/255, alpha: 1.0)
        static let replyColor = UIColor(red: 84/255, green: 233/255, blue: 244/255, alpha: 1.0)
        static let retweetColor = UIColor(red: 25/255, green: 207/255, blue: 134/255, alpha: 1.0)
        static let selectedColor = UIColor(red: 170/255, green: 184/255, blue: 194/255, alpha: 1.0)
        static let deleteColor = UIColor(red: 1.0, green: 0, blue: 0, alpha: 1.0)
        static let twitterColor = UIColor(red: 85/255, green: 172/255, blue: 238/255, alpha:1.0)
    }
    enum Saveword {
        static let image = "tweet_image"
        static let twitter = "twitter_account"
        static let search = "search_word"
    }
}

AutoLayoutについて

こことか見て、あと画像サイズの関係でコードからframeの長さとか扱わなきゃいけない部分に関してはIntへの直し方を参考にしました。
LaunchScreen.xibについてはiOS7か8あたりからの対応なので本当は昔のiOS用に起動画像を用意するべきなんですが、iOSの驚異のアップデート率は毎回のWWDCやらApple発表会やらで発表されてる通りなので気にしないことにします()

デバッグに関して

新幹線の先頭車両みたいなアイコンをXcodeのプロジェクト開いてる時にぽちっとおしたりするとブレークポイントのタブになります。ここで下の+からAdd Exception Breakpointとかを選択しAllとかにしておくとエラーが出たところにブレークポイントを自動でつけてくれるそうです。

リリースに関して

まず申請の流れで参考になる記事は僕のストックにあるので探ってください。
で、躓いたところ。
①Missing iOS うんにゃらとでてVerifyできない
→エラーメッセージで検索すると出てくるようにキーチェーンの期限切れ証明書を消して新しい物にする
②ビルドが無効ですと出てアップロードしたけど提出できない
→ライブラリ周りでつまってることが大半です。Pod、Carthageを最新版にする、ライブラリを最新版にするなどはもちろん、それらとは別にライブラリを入れている人はSwifterのところにリンクを貼った参考サイトをみてしっかり紐付けしましょう。特にCopy filesの設定をしていないとデバッグ時点では出来るのにいざ提出してみると実はライブラリがついてなかったみたいなことになりかねないので気をつけましょう(実際僕もこれにやられました。)
③用意すべき画像のフォーマットに関する参考記事も僕のストックにあります。
④デベロッパー登録で謎のエラーが出て問い合わせて、最終的に「エンジニアに問い合わせることになります」とかなったら潔く新しくAppleIDつくるのが良いと思います。
⑤Appleからの電話、クパチーノとかからかかってきてビビることもありますが担当者はちゃんと日本人です。少なくともユーザーサポートに関しては。

また余談ですがLaunchscreenで使う画像はなるべくAssetカタログなどに登録しておいたほうがいいみたいです。起動した時点で画像がロードされてなくてうつらないみたいなことになってしまいます(なりました。。。)

参考リンク

UI/UXの参考として教えてもらったサイト集です。

Qiitaはたくさんストックしてるのでそちらを見てください。

Tipsとか系

ライセンス関連

広告

その他

その他つまづいたとこ

  • ブレークポイントを間違って設定してしまったら右クリックするとDelete BrakePointと出てくるからそれで消せる
  • TableViewやTabBarのDelegateやDataSourceはちゃんとストーリーボードで入ってるViewControllerとつなげておかないとエラーないのに表示されないとかなってくるので注意
  • UIButtonで画像変えたいとかだったらここは一応見ておくべき
  • 大抵の通信処理は非同期処理なのでclassとかにして処理を一括しておきたい場合はライブラリとかの真似してSuccessHandlerみたいなのをとれるようにしておこう
  • UIColorのredなどの値の範囲は0〜255ではなくあくまで0〜1.0なので上みたいに割っておく必要がある
  • NavigationControllerを設定すると中のViewに自動でナビゲーションバー分のマージンが自動で追加されるようなので画面上端まで伸ばしてあげる必要があるようです。
  • NavigationControllerとTabBarControllerを併用するときはTabBarControllerを先に置くようにするとよいっぽい。詳しくはQiitaのストック参照
  • CustomCellからtouchesEndedをオーバーライドして使ってみたけど上手く反応してくれない...
  • backgroundColorとbackTintColorとか色々違うからNavigationBarとかの色変更にはここ見てやったほうがいい
20
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?