好吧,显然,我们无法创建一个新项目,粘贴您发布的代码,然后运行它。
所以...希望这会有所帮助。
滚动视图的.bounds
is the 可见矩形 of its .contentSize
.
所以,如果我们举这个例子:
- 创建一个 400x600“mapView”(作为
viewForZooming
)
- 添加一个 30x30“标记”子视图
origin: x: 240, y: 400
- 使用 200 x 300 框架的滚动视图(黄色背景)
- 将mapView的所有4个边限制为scrollview的
.contentLayoutGuide
从 1.0 缩放开始,它看起来像这样(当然,滚动视图框架之外的所有内容都将被隐藏):
滚动视图将具有:
ContentSize: (400.0, 600.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
如果我们一直滚动到右下角,它将如下所示:
with:
ContentSize: (400.0, 600.0)
Bounds: (200.0, 300.0, 200.0, 300.0)
如果我们放大到 2.0 缩放比例,我们会得到:
ContentSize: (800.0, 1200.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
ContentSize: (800.0, 1200.0)
Bounds: (600.0, 900.0, 200.0, 300.0)
如果我们放大到 3.0 缩放比例,我们会得到:
ContentSize: (1200.0, 1800.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
ContentSize: (1200.0, 1800.0)
Bounds: (1000.0, 1500.0, 200.0, 300.0)
如果我们放大out至 0.5 缩放比例:
ContentSize: (200.0, 300.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
第一个任务是查明“标记”是否可见......如果是,我们不需要做任何事情。如果它是not可见(滚动到框架之外),我们希望将其居中。
所以,如果我们像这样滚动:
我们可以说:
let r = marker.frame
let isInside = scrollView.bounds.contains(r)
在这种情况下,isInside
将true
.
但是,如果标记是outside像这样的框架:
我们想要定义一个CGRect
它与滚动视图的边界具有相同的宽度和高度,以标记的中心为中心:
我们可以调用:
scrollView.scrollRectToVisible(r, animated: true)
当然,如果我们的标记视图靠近边缘,如下所示:
这是尽可能接近中心的位置。
那不是quite不过够了...
标记视图的框架将始终是其自己的框架 - 当滚动视图的缩放比例发生变化时,它会缩放。因此,我们需要考虑到这一点:
let r = marker.frame.applying(CGAffineTransform(scaleX: self.scrollView.zoomScale, y: self.scrollView.zoomScale))
let isInside = scrollView.bounds.contains(r)
这是一个完整的示例演示...我们创建两倍于滚动视图大小的“地图视图”,minZoom:0.5,maxZoom:3.0,排列“标记”图案,并使用两个按钮“如果需要则突出显示和居中” ”:
Note: 仅示例代码- 不适合“生产就绪”:
class CenterInScrollVC: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
return v
}()
// can be any type of view
// using a "Dashed Outline" so we can see its edges
let mapView: DashView = {
let v = DashView()
v.backgroundColor = UIColor(white: 0.9, alpha: 0.5)
v.color = .blue
v.style = .border
return v
}()
var mapMarkers: [UIView] = []
var markerIndex: Int = 0
// let's make the markers 40x40
let markerSize: CGFloat = 40.0
// percentage of one-half of marker that must be visible to NOT screll to center
// 1.0 == entire marker must be visible
// 0.5 == up to 1/4 of marker may be out of view
// <= 0.0 == only check that the Center of the marker is in view
// can be set to > 1.0 to require entire marker Plus some "padding"
let pctVisible: CGFloat = 1.0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// a button to center the current marker (if needed)
let btnA: UIButton = {
let v = UIButton()
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.setTitle("Center Current if Needed", for: [])
v.addTarget(self, action: #selector(btnATap(_:)), for: .touchUpInside)
return v
}()
// a button to select the next marker, center if needed
let btnB: UIButton = {
let v = UIButton()
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.setTitle("Go To Marker - 2", for: [])
v.addTarget(self, action: #selector(btnBTap(_:)), for: .touchUpInside)
return v
}()
// add a view with a "+" marker to show the center of the scroll view
let centerView: DashView = {
let v = DashView()
v.backgroundColor = .clear
v.color = UIColor(red: 0.95, green: 0.2, blue: 1.0, alpha: 0.5)
v.style = .centerMarker
v.isUserInteractionEnabled = false
return v
}()
[btnA, btnB, mapView, scrollView, centerView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
[btnA, btnB, scrollView, centerView].forEach { v in
view.addSubview(v)
}
scrollView.addSubview(mapView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// buttons at the top
btnA.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
btnA.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.7),
btnA.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
btnB.topAnchor.constraint(equalTo: btnA.bottomAnchor, constant: 20.0),
btnB.widthAnchor.constraint(equalTo: btnA.widthAnchor),
btnB.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
// let's inset the scroll view to make it easier to distinguish
scrollView.topAnchor.constraint(equalTo: btnB.bottomAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -40.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -40.0),
// overlay "center lines" view
centerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
centerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
centerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
centerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
// mapView Top/Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
mapView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
mapView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
mapView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
mapView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// let's make the mapView twice as wide and tall as the scroll view
mapView.widthAnchor.constraint(equalTo: frameG.widthAnchor, multiplier: 2.0),
mapView.heightAnchor.constraint(equalTo: frameG.heightAnchor, multiplier: 2.0),
])
// some example locations for the Markers
let pcts: [[CGFloat]] = [
[0.50, 0.50],
[0.25, 0.50],
[0.50, 0.25],
[0.75, 0.50],
[0.50, 0.75],
[0.10, 0.15],
[0.90, 0.15],
[0.90, 0.85],
[0.10, 0.85],
]
for (i, p) in pcts.enumerated() {
let v = UILabel()
v.text = "\(i + 1)"
v.textAlignment = .center
v.textColor = .yellow
v.backgroundColor = .systemBlue
v.font = .systemFont(ofSize: 15.0, weight: .bold)
v.translatesAutoresizingMaskIntoConstraints = false
mapMarkers.append(v)
mapView.addSubview(v)
v.widthAnchor.constraint(equalToConstant: markerSize).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
NSLayoutConstraint(item: v, attribute: .centerX, relatedBy: .equal, toItem: mapView, attribute: .trailing, multiplier: p[0], constant: 0.0).isActive = true
NSLayoutConstraint(item: v, attribute: .centerY, relatedBy: .equal, toItem: mapView, attribute: .bottom, multiplier: p[1], constant: 0.0).isActive = true
}
scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 3.0
scrollView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// let's start with the scroll view zoomed out
scrollView.zoomScale = scrollView.minimumZoomScale
// highlight and center (if needed) the 1st marker
markerIndex = 0
let marker = mapMarkers[markerIndex % mapMarkers.count]
highlightMarkerAndCenterIfNeeded(marker, animated: true)
}
@objc func btnATap(_ sender: Any?) {
// to easily test "center if not visible" without changing the "current marker"
let marker = mapMarkers[markerIndex % mapMarkers.count]
highlightMarkerAndCenterIfNeeded(marker, animated: true)
}
@objc func btnBTap(_ sender: Any?) {
// increment index to the next marker
markerIndex += 1
let marker = mapMarkers[markerIndex % mapMarkers.count]
// center if needed
highlightMarkerAndCenterIfNeeded(marker, animated: true)
// update button title
if let b = sender as? UIButton, let m = mapMarkers[(markerIndex + 1) % mapMarkers.count] as? UILabel, let t = m.text {
b.setTitle("Go To Marker - \(t)", for: [])
}
}
func highlightMarkerAndCenterIfNeeded(_ marker: UIView, animated: Bool) {
// "un-highlight" all markers
mapMarkers.forEach { v in
v.backgroundColor = .systemBlue
}
// "highlight" the new marker
marker.backgroundColor = .systemGreen
// get the marker frame, scaled by zoom scale
var r = marker.frame.applying(CGAffineTransform(scaleX: self.scrollView.zoomScale, y: self.scrollView.zoomScale))
// inset the rect if we allow less-than-full marker visible
if pctVisible > 0.0 {
let iw: CGFloat = (1.0 - pctVisible) * r.width * 0.5
let ih: CGFloat = (1.0 - pctVisible) * r.height * 0.5
r = r.insetBy(dx: iw, dy: ih)
}
var isInside: Bool = true
if pctVisible <= 0.0 {
// check center point only
isInside = self.scrollView.bounds.contains(CGPoint(x: r.midX, y: r.midY))
} else {
// check the rect
isInside = self.scrollView.bounds.contains(r)
}
// if the marker rect (or point) IS inside the scroll view
// we don't do anything
// if it's NOT inside the scroll view
// center it
if !isInside {
// create a rect using scroll view's bounds centered on marker's center
let w: CGFloat = self.scrollView.bounds.width
let h: CGFloat = self.scrollView.bounds.height
r = CGRect(x: r.midX, y: r.midY, width: w, height: h).offsetBy(dx: -w * 0.5, dy: -h * 0.5)
if animated {
// let's slow down the animation a little
UIView.animate(withDuration: 0.75, delay: 0.0, options: [.curveEaseInOut], animations: {
self.scrollView.scrollRectToVisible(r, animated: false)
}, completion: nil)
} else {
self.scrollView.scrollRectToVisible(r, animated: false)
}
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return mapView
}
}
Edit-- 正如代码注释中所述,任何视图都可以用于viewForZooming
,但这是代码DashView
I used:
class DashView: UIView {
// border or
// vertical and horizontal center lines or
// two lines forming a + in the center
enum Style: Int {
case border
case centerLines
case centerMarker
}
public var style: Style = .border {
didSet {
setNeedsLayout()
}
}
// solid or dashed
public var solid: Bool = false
// line color
public var color: UIColor = .yellow {
didSet {
dashLayer.strokeColor = color.cgColor
}
}
private let dashLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
layer.addSublayer(dashLayer)
dashLayer.strokeColor = color.cgColor
dashLayer.fillColor = UIColor.clear.cgColor
dashLayer.lineWidth = 2
}
override func layoutSubviews() {
super.layoutSubviews()
var bez = UIBezierPath()
switch style {
case .border:
bez = UIBezierPath(rect: bounds)
dashLayer.lineDashPattern = [10, 10]
case .centerLines:
bez.move(to: CGPoint(x: bounds.midX, y: bounds.minY))
bez.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
bez.move(to: CGPoint(x: bounds.minX, y: bounds.midY))
bez.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY))
dashLayer.lineDashPattern = [10, 10]
case .centerMarker:
bez.move(to: CGPoint(x: bounds.midX, y: bounds.midY - 40.0))
bez.addLine(to: CGPoint(x: bounds.midX, y: bounds.midY + 40.0))
bez.move(to: CGPoint(x: bounds.midX - 40.0, y: bounds.midY))
bez.addLine(to: CGPoint(x: bounds.midX + 40.0, y: bounds.midY))
dashLayer.lineDashPattern = []
}
if solid {
dashLayer.lineDashPattern = []
}
dashLayer.path = bez.cgPath
}
}