LoginSignup
18
21

More than 5 years have passed since last update.

【iOS】SmartNewsみたいなUIをつくってみました

Posted at

はじめに

iOS Second Stage Advent Calendar 12日目の記事です。

今回で3記事目です。

宜しくお願いします。

本題

今回はSmartNewsみたいなUIをサンプルでつくってみました。

以下のような感じです。

SampleSmartNewsUI.gif

ありきたりのUIPageViewControllerを使ったところではなくて、各ページの(縦の)スクロールに合わせてヘッダーの部分が動くところを実装してみました。

サンプルプロジェクトを GitHubにあげたので、よかったら見てみてください。

AdventCalendar2015/SampleSmartNewsUI at master · ryokosuge/AdventCalendar2015

細かい解説

プロジェクトをXcodeで見てもらえればすぐわかると思うのですが、ちょっとした構成を...。

Storyboard

以下のようになっています。

スクリーンショット 2015-12-10 22.11.07.png

ContainerViewのところにUIPageViewControllerviewaddSubview()しています。

ここら辺はソースコードを見た方が早いかと思います。

ソースコード

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var progressView: UIView!
    @IBOutlet weak var progressBarView: UIView!
    @IBOutlet weak var progressTextLabel: UILabel!
    @IBOutlet weak var navigationBar: UINavigationBar!
    @IBOutlet weak var menuView: UIView!
    @IBOutlet weak var containerView: UIView!

    @IBOutlet weak var constProgressViewHeight: NSLayoutConstraint!
    @IBOutlet weak var constMenuViewTopMargin: NSLayoutConstraint!
    @IBOutlet weak var constProgressBarViewWidth: NSLayoutConstraint!

    private var pageViewController: UIPageViewController? = nil

    private let MaxPage: Int = 10
    private let MaxContentInset = UIEdgeInsets(top: 88, left: 0, bottom: 0, right: 0)
    private let DefaultProgressViewHeight: CGFloat = 64.0
    private let PulledDownTextInterval: CGFloat = -70.0
    private let ReleasedTextInterval: CGFloat = -120.0

    private var currentIndex: Int = 0
    private var isLoading: Bool = false {
        didSet {
            progressTextLabel.text = isLoading ? "更新中" : "Sample"
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setupView()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: true)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

/// MARK: - PageViewController Delegate.
extension ViewController: ContentViewControllerDelegate {

    func didScrollOffset(offset: CGPoint, pageIndex: Int) {
        let offsetY = offset.y + MaxContentInset.top
        let barHeight = navigationBar.frame.height
        if offsetY < barHeight {
            if offsetY <= 0 {
                let progressViewHeight = DefaultProgressViewHeight - offsetY
                constProgressViewHeight.constant = progressViewHeight
                constMenuViewTopMargin.constant = 0
            } else {
                constMenuViewTopMargin.constant = -offsetY
                constProgressViewHeight.constant = DefaultProgressViewHeight
            }
        } else {
            constMenuViewTopMargin.constant = -navigationBar.frame.height
        }

        if !isLoading {
            if offsetY < ReleasedTextInterval {
                progressTextLabel.text = "はなして更新"
            } else if offsetY < PulledDownTextInterval {
                progressTextLabel.text = "引き下げて更新"
            } else {
                progressTextLabel.text = "Sample"
            }
        }
    }

    func didSelectIndexPath(indexPath: NSIndexPath, item: String) {
        performSegueWithIdentifier("ShowDetail", sender: nil)
    }

    func didEndDraggingOffset(offset: CGPoint, pageIndex: Int) {
        if !isLoading {
            let offsetY = offset.y + MaxContentInset.top
            if offsetY < ReleasedTextInterval {
                fetch()
            }
        }
    }
}

/// MARK: - UIContentViewController Delegate.
extension ViewController: UIPageViewControllerDelegate {

    func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if let viewController = pageViewController.viewControllers?.first as? ContentViewController {
            currentIndex = viewController.index
        }
    }

}


/// MARK: - UIPageViewController DataSource
extension ViewController: UIPageViewControllerDataSource {

    func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
        let beforeIndex = currentIndex - 1
        return viewControllerAtIndex(beforeIndex)
    }

    func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
        let afterIndex = currentIndex + 1
        return viewControllerAtIndex(afterIndex)
    }

}

/// MARK: - private methods.
extension ViewController {

    private func fetch() {
        isLoading = true
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {[weak self] () -> Void in
            var progress: CGFloat = 0.0
            while (progress < 100.0) {
                let rand = CGFloat(Int(arc4random_uniform(10)))
                progress += rand
                dispatch_async(dispatch_get_main_queue(), {[weak self] () -> Void in
                    self?.updateProgressBarPercentage((progress / 100.0))
                })
                NSThread.sleepForTimeInterval(0.05)
            }
            dispatch_async(dispatch_get_main_queue(), {[weak self] () -> Void in
                self?.updateProgressBarPercentage(progress)
            })

            dispatch_async(dispatch_get_main_queue(), {[weak self] () -> Void in
                self?.finihed()
            })

        }
    }

    private func finihed() {
        isLoading = false
        UIView.animateWithDuration(0.3, animations: {[weak self] () -> Void in
            self?.progressBarView.alpha = 0.0
        }) {[weak self] (finished) -> Void in
            self?.constProgressBarViewWidth.constant = 0
            self?.progressBarView.alpha = 1.0
        }
    }

    private func updateProgressBarPercentage(percentage: CGFloat) {
        let progress = progressView.frame.width * percentage
        constProgressBarViewWidth.constant = progress
    }

    /// Viewに関することを処理する
    private func setupView() {
        /// navigationControllerのnavigationBarは非表示にする
        navigationController?.setNavigationBarHidden(true, animated: false)

        /// 自前のnavigationBarの設定
        navigationBar.translucent = true
        navigationBar.setBackgroundImage(UIImage(), forBarMetrics: UIBarMetrics.Default)
        navigationBar.shadowImage = UIImage()

        let pageViewController = UIPageViewController(transitionStyle: UIPageViewControllerTransitionStyle.Scroll, navigationOrientation: UIPageViewControllerNavigationOrientation.Horizontal, options: nil)
        pageViewController.willMoveToParentViewController(self)
        pageViewController.delegate = self
        pageViewController.dataSource = self
        pageViewController.view.frame = containerView.bounds
        pageViewController.view.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
        containerView.addSubview(pageViewController.view)
        addChildViewController(pageViewController)
        pageViewController.didMoveToParentViewController(self)
        self.pageViewController = pageViewController

        if let viewController = viewControllerAtIndex(currentIndex) {
            pageViewController.setViewControllers([viewController], direction: .Forward, animated: false, completion: nil)
        }
    }

    private func viewControllerAtIndex(index: Int) -> ContentViewController? {

        if index < 0 || MaxPage < index {
            return nil
        }

        let viewController = ContentViewController.instantiateViewControllerAtIndex(index)
        viewController.delegate = self
        viewController.setInset(MaxContentInset)
        viewController.setOffset(CGPoint(x: 0, y: -MaxContentInset.top))

        return viewController
    }
}

UIPageViewControllerで表示しているViewはこんな感じです。

ContentViewController
import UIKit

protocol ContentViewControllerDelegate: class {

    func didScrollOffset(offset: CGPoint, pageIndex: Int)
    func didSelectIndexPath(indexPath: NSIndexPath, item: String)
    func didEndDraggingOffset(offset: CGPoint, pageIndex: Int)

}

class ContentViewController: UIViewController {

    static func instantiateViewControllerAtIndex(index: Int) -> ContentViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let viewController = storyboard.instantiateViewControllerWithIdentifier("ContentViewController") as! ContentViewController
        viewController.index = index
        return viewController
    }

    @IBOutlet weak var tableView: UITableView!
    weak var delegate: ContentViewControllerDelegate? = nil

    private let maxCellCount = 100
    private(set) var index: Int = 0
    private var items: [String] = []

    private var _offset: CGPoint? = nil
    private var _inset: UIEdgeInsets? = nil

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        setupView()
        createItems()
    }

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)

        if let o = _offset {
            tableView.contentOffset = o
            _offset = nil
        }

        if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
            tableView.deselectRowAtIndexPath(indexPathForSelectedRow, animated: true)
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

/// MARK : - public methods.
extension ContentViewController {

    func setOffset(offset: CGPoint) {
        if let t = tableView {
            t.contentOffset = offset
        } else {
            _offset = offset
        }
    }

    func setInset(inset: UIEdgeInsets) {
        if let t = tableView {
            t.contentInset = inset
        } else {
            _inset = inset
        }
    }
}

extension ContentViewController: UITableViewDataSource {

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let string = items[indexPath.row]
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
        cell.textLabel?.text = string
        return cell
    }
}

extension ContentViewController: UITableViewDelegate {

    func scrollViewDidScroll(scrollView: UIScrollView) {
        delegate?.didScrollOffset(scrollView.contentOffset, pageIndex: index)
        let insetTop = _inset?.top ?? 0
        if scrollView.contentOffset.y < -insetTop {
            let inset = insetTop - (insetTop + scrollView.contentOffset.y)
            tableView.scrollIndicatorInsets.top = inset
        } else {
            tableView.scrollIndicatorInsets.top = insetTop
        }
    }

    func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        delegate?.didEndDraggingOffset(scrollView.contentOffset, pageIndex: index)
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let item = items[indexPath.row]
        delegate?.didSelectIndexPath(indexPath, item: item)
    }
}

/// MARK: - private methods.
extension ContentViewController {

    private func setupView() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.tableFooterView = UIView()

        if let i = _inset {
            tableView.contentInset = i
            tableView.scrollIndicatorInsets = i
        }

        if let o = _offset {
            tableView.contentOffset = o
        }
    }

    private func createItems() {
        self.items = (0..<maxCellCount).map({ "cell \($0)"})
    }

}

という感じです。

補足

簡単に説明します。

イベントの始まりはContentViewControllertableViewのscrollイベントです。

scrollViewDidScroll(scrollView: UIScrollView)が起きたらdelegateViewControllerにイベントを渡しています。

で、ContentViewControllerDelegatedidScrollOffset(offset: CGPoint, index: Int)からスクロールしたオフセットを計算して各Viewを動かしているという形です。

終わりに

最終的にはプロジェクトをみて動かしてもらえると嬉しいです。

AdventCalendar2015/SampleSmartNewsUI at master · ryokosuge/AdventCalendar2015

以上になります。

18
21
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
18
21