在iOS中绘制无限网格

时间:2018-07-26 17:36:46

标签: ios

我想知道在我正在构建的iOS应用上创建无限网格的最佳方法。我在iPhone上使用内部硬件来收集实际数据并构建矢量。我想在网格上直观地表示矢量数据,就像图表一样。每个向量都表示为一条线,每个新向量都附加到此网格上的先前向量。我正在构建该应用程序,以便它可以运行用户想要的任何时间(从几分钟到几小时)。我遇到的唯一问题是如何从此网格开始。我想如果用户将应用程序运行几个小时,则该应用程序上的行会变得很长,因此网格需要容纳它并以这种方式“无限”。

通过无限,我的意思是用户可以在网格上向上,向下,向左或向右滑动,并且组成网格的线永远不会结束。无论他们在何处滑动或滑动多长时间,屏幕上总会有一个网格。如前所述,网格还应具有图形的属性。我希望能得到类似的结果:https://bl.ocks.org/mbostock/6123708,但此图不完整。

我做了一些研究,大多数关于网格的问题都来自SpriteKit框架。但是我不知道运行游戏引擎是否是最佳解决方案。我想使用核心图形,但是如果无法在该框架上执行此操作,则可以提出其他建议。感谢您从何处开始提供任何帮助!

1 个答案:

答案 0 :(得分:0)

我创建了一个示例项目来说明需要做什么。可以在以下位置找到该代码:https://github.com/ekscrypto/Infinite-Grid-Swift

从本质上讲,您从一个非常简单的UIScrollView开始,在其中您分配了一个“引用”视图,该视图最初将成为(0,0)点。然后,您在参考视图和滚动视图内容边缘之间设置了一个大得离谱的距离(足够大,因此用户可能无法不停地不停滚动)并调整contentOffset,以使您的视图适合滚动视图的中间位置。

然后,您必须观察scrollview的contentOffset并找出每边需要多少个图块来填充屏幕以及更多内容,以便在用户滚动时始终显示要显示的内容。可以将其设置为任意数量的图块,但请注意保持合理性,因为您的图块可能会消耗内存。我发现1个全屏的宽度/高度足以应付最快的手动滚动。

随着用户滚动,将调用contentOffset观察器,使您可以根据需要添加或删除视图。

滚动视图完成动画设置后,您将需要重置参考点,以免耗尽contentOffset来使用。

假设有一个相对简单的“ GridTile”类,将实例化该类以填充网格:

protocol GridTileDataSource {
    func contentView(for: GridTile) -> UIView?
}

class GridTile: UIView {

    let coordinates: (Int, Int)

    private let dataSource: GridTileDataSource

    // Custom initializer
    init(frame: CGRect, coordinates: (Int, Int), dataSource: GridTileDataSource) {
        self.coordinates = coordinates
        self.dataSource = dataSource
        super.init(frame: frame)
        self.backgroundColor = UIColor.clear
        self.isOpaque = false
    }

    // Unused, not supporting Xib/Storyboard
    required init?(coder aDecoder: NSCoder) {
        return nil
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        populateWithContent()
    }

    private func populateWithContent() {
        if self.subviews.count == 0,
            let subview = dataSource.contentView(for: self) {
            subview.frame = self.bounds
            self.addSubview(subview)
        }
    }
}

并从相对简单的UIView / UIScrollView设置开始: enter image description here

您可以这样创建GridView机制:

class GridView: UIView {
    @IBOutlet weak var hostScrollView: UIScrollView?
    @IBOutlet weak var topConstraint: NSLayoutConstraint?
    @IBOutlet weak var bottomConstraint: NSLayoutConstraint?
    @IBOutlet weak var leftConstraint: NSLayoutConstraint?
    @IBOutlet weak var rightConstraint: NSLayoutConstraint?

    private(set) var allocatedTiles: [GridTile] = []
    private(set) var referenceCoordinates: (Int, Int) = (0,0)
    private(set) var tileSize: CGFloat = 0.0

    private(set) var observingScrollview: Bool = false
    private(set) var centerCoordinates: (Int, Int) = (Int.max, Int.max)

    deinit {
        if observingScrollview {
            hostScrollView?.removeObserver(self, forKeyPath: "contentOffset")
        }
    }

    func populateGrid(size tileSize: CGFloat, center: (Int, Int)) {
        clearGrid()
        self.referenceCoordinates = center
        self.tileSize = tileSize
        observeScrollview()
        adjustScrollviewInsets()
    }

    private func clearGrid() {
        for tile in allocatedTiles {
            tile.removeFromSuperview()
        }
        allocatedTiles.removeAll()
    }

    private func observeScrollview() {
        guard observingScrollview == false,
            let scrollview = hostScrollView
            else { return }
        scrollview.delegate = self
        scrollview.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
        observingScrollview = true
    }

    private func adjustScrollviewInsets() {
        guard let scrollview = hostScrollView else { return }

        // maximum continous user scroll before hitting the scrollview edge
        // set this to something small (~3000) to observe the scrollview indicator resetting to middle
        let arbitraryLargeOffset: CGFloat = 10000000.0
        topConstraint?.constant = arbitraryLargeOffset
        bottomConstraint?.constant = arbitraryLargeOffset
        leftConstraint?.constant = arbitraryLargeOffset
        rightConstraint?.constant = arbitraryLargeOffset
        scrollview.layoutIfNeeded()
        let xOffset = arbitraryLargeOffset - ((scrollview.frame.size.width - self.frame.size.width) * 0.5)
        let yOffset = arbitraryLargeOffset - ((scrollview.frame.size.height - self.frame.size.height) * 0.5)
        scrollview.setContentOffset(CGPoint(x: xOffset, y: yOffset), animated: false)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let scrollview = object as? UIScrollView else { return }
        adjustGrid(for: scrollview)
    }

    private func adjustGrid(for scrollview: UIScrollView) {
        let center = computedCenterCoordinates(scrollview)
        guard center != centerCoordinates else { return }
        self.centerCoordinates = center
        //print("center is now at coordinates: \(center)")
        // pre-allocate views past the bounds of the visible scrollview so when user
        // drags the view, even super-quick, there is content to show
        let xCutoff = Int(((scrollview.frame.size.width * 1.5) / tileSize).rounded(.up))
        let yCutoff = Int(((scrollview.frame.size.height * 1.5) / tileSize).rounded(.up))
        let lowerX = center.0 - xCutoff
        let upperX = center.0 + xCutoff
        let lowerY = center.1 - yCutoff
        let upperY = center.1 + yCutoff
        clearGridOutsideBounds(lowerX: lowerX, upperX: upperX, lowerY: lowerY, upperY: upperY)
        populateGridInBounds(lowerX: lowerX, upperX: upperX, lowerY: lowerY, upperY: upperY)
    }

    private func computedCenterCoordinates(_ scrollview: UIScrollView) -> (Int, Int) {
        guard tileSize > 0 else { return centerCoordinates }
        let contentOffset = scrollview.contentOffset
        let scrollviewSize = scrollview.frame.size
        let xOffset = -(self.center.x - (contentOffset.x + scrollviewSize.width * 0.5))
        let yOffset = -(self.center.y - (contentOffset.y + scrollviewSize.height * 0.5))
        let xIntOffset = Int((xOffset / tileSize).rounded())
        let yIntOffset = Int((yOffset / tileSize).rounded())
        return (xIntOffset + referenceCoordinates.0, yIntOffset + referenceCoordinates.1)
    }

    private func clearGridOutsideBounds(lowerX: Int, upperX: Int, lowerY: Int, upperY: Int) {
        let tilesToProcess = allocatedTiles
        for tile in tilesToProcess {
            let tileX = tile.coordinates.0
            let tileY = tile.coordinates.1
            if tileX < lowerX || tileX > upperX || tileY < lowerY || tileY > upperY {
//                print("Deallocating grid tile: \(tile.coordinates)")
                tile.removeFromSuperview()
                if let index = allocatedTiles.index(of: tile) {
                    allocatedTiles.remove(at: index)
                }
            }
        }
    }

    private func populateGridInBounds(lowerX: Int, upperX: Int, lowerY: Int, upperY: Int) {
        guard upperX > lowerX, upperY > lowerY else { return }
        var coordX = lowerX
        while coordX <= upperX {
            var coordY = lowerY
            while coordY <= upperY {
                allocateTile(at: (coordX, coordY))
                coordY += 1
            }
            coordX += 1
        }
    }

    private func allocateTile(at tileCoordinates: (Int, Int)) {
        guard existingTile(at: tileCoordinates) == nil else { return }
//        print("Allocating grid tile: \(tileCoordinates)")
        let tile = GridTile(frame: frameForTile(at: tileCoordinates),
                            coordinates: tileCoordinates,
                            dataSource: self)
        allocatedTiles.append(tile)
        self.addSubview(tile)
    }

    private func existingTile(at coordinates: (Int, Int)) -> GridTile? {
        for tile in allocatedTiles where tile.coordinates == coordinates {
            return tile
        }
        return nil
    }

    private func frameForTile(at coordinates: (Int, Int)) -> CGRect {
        let xIntOffset = coordinates.0 - referenceCoordinates.0
        let yIntOffset = coordinates.1 - referenceCoordinates.1
        let xOffset = self.bounds.size.width * 0.5 + (tileSize * (CGFloat(xIntOffset) - 0.5))
        let yOffset = self.bounds.size.height * 0.5 + (tileSize * (CGFloat(yIntOffset) - 0.5))
        return CGRect(x: xOffset, y: yOffset, width: tileSize, height: tileSize)
    }

    // readjustOffsets() should only be called when the scrollview is not animating to
    // avoid any jerky movement.
    private func readjustOffsets() {
        guard
            centerCoordinates != referenceCoordinates,
            let scrollview = hostScrollView,
            tileSize > 0
            else { return }
        let xOffset = CGFloat(centerCoordinates.0 - referenceCoordinates.0) * tileSize
        let yOffset = CGFloat(centerCoordinates.1 - referenceCoordinates.1) * tileSize
        referenceCoordinates = centerCoordinates
        for tile in allocatedTiles {
            var frame = tile.frame
            frame.origin.x -= xOffset
            frame.origin.y -= yOffset
            tile.frame = frame
        }
        var newContentOffset = scrollview.contentOffset
        newContentOffset.x -= xOffset
        newContentOffset.y -= yOffset
        scrollview.setContentOffset(newContentOffset, animated: false)
    }
}

extension GridView: UIScrollViewDelegate {
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        guard decelerate == false else { return }
        self.readjustOffsets()
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        self.readjustOffsets()
    }
}

extension GridView: GridTileDataSource {

    // This is where you would provide the content to put in the tiles, could be
    // maps, images, whatever.  In this case went with a simple label containing the coordinates
    internal func contentView(for tile: GridTile) -> UIView? {
        let placeholderLabel = UILabel(frame: tile.bounds)
        let coordinates = tile.coordinates
        placeholderLabel.text = "\(coordinates.0, coordinates.1)"
        placeholderLabel.textColor = UIColor.blue
        placeholderLabel.textAlignment = .center
        return placeholderLabel
    }
}

剩下的就是通过指定网格大小和要使用的初始坐标来启动GridView:

class ViewController: UIViewController {

    @IBOutlet weak var gridView: GridView?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        gridView?.populateGrid(size: 150.0, center: (0,0))
    }
}

在那里,有一个无限的网格。

干杯和好运!