我想知道在我正在构建的iOS应用上创建无限网格的最佳方法。我在iPhone上使用内部硬件来收集实际数据并构建矢量。我想在网格上直观地表示矢量数据,就像图表一样。每个向量都表示为一条线,每个新向量都附加到此网格上的先前向量。我正在构建该应用程序,以便它可以运行用户想要的任何时间(从几分钟到几小时)。我遇到的唯一问题是如何从此网格开始。我想如果用户将应用程序运行几个小时,则该应用程序上的行会变得很长,因此网格需要容纳它并以这种方式“无限”。
通过无限,我的意思是用户可以在网格上向上,向下,向左或向右滑动,并且组成网格的线永远不会结束。无论他们在何处滑动或滑动多长时间,屏幕上总会有一个网格。如前所述,网格还应具有图形的属性。我希望能得到类似的结果:https://bl.ocks.org/mbostock/6123708,但此图不完整。
我做了一些研究,大多数关于网格的问题都来自SpriteKit框架。但是我不知道运行游戏引擎是否是最佳解决方案。我想使用核心图形,但是如果无法在该框架上执行此操作,则可以提出其他建议。感谢您从何处开始提供任何帮助!
答案 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设置开始:
您可以这样创建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))
}
}
在那里,有一个无限的网格。
干杯和好运!