You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用UIKit实现Apple Maps悬浮按钮,解决半Sheet偏移跳变问题

实现Apple Maps风格悬浮按钮:解决半Sheet高度变化时按钮偏移跳变问题

我想用UIKit实现类似Apple Maps的悬浮操作按钮,现在卡在半Sheet高度动态变化时,按钮偏移量同步平滑变化的效果还原上。

我通过父视图控制器的sheetPresentationController展示半Sheet,重写子视图控制器的viewWillLayoutSubviews方法获取视图新高度,以此设置悬浮按钮的偏移量。但遇到问题:当用户提前松开Sheet时,按钮偏移会出现跳变(比如视图高度从200跳至500),没法像Apple Maps那样平滑过渡。请问怎么获取子视图高度的更细粒度更新?

Apple Maps中的效果演示:
Apple Maps悬浮按钮效果

我的项目中跳变效果演示:
项目中按钮偏移跳变效果

简化示例项目代码

class ViewController: UIViewController {
  private var subscriptions = Set<AnyCancellable>()
  private var buttonBottomConstraint = NSLayoutConstraint()
  
  private lazy var floatingButton: UIButton = {
    let button = UIButton()
    
    button.setImage(
      UIImage(systemName: "list.dash"), for: .normal
    )
    
    button.addTarget(
      self,
      action: #selector(openSheet),
      for: .touchUpInside
    )
    
    button.translatesAutoresizingMaskIntoConstraints = false
    button.backgroundColor = .systemBackground.withAlphaComponent(0.95)
    button.layer.cornerRadius = 20.0
    button.layer.shadowColor = UIColor.black.cgColor
    button.layer.shadowOpacity = 0.5
    button.layer.shadowOffset = CGSize(width: 0, height: 2)
    button.layer.shadowRadius = 4.0
    button.layer.shouldRasterize = true
    button.layer.rasterizationScale = UIScreen.main.scale
    return button
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    configureView()
  }
  
  private func configureView() {
    view.backgroundColor = .systemMint
    view.addSubview(floatingButton)
    
    buttonBottomConstraint = floatingButton.bottomAnchor.constraint(
      equalTo: view.bottomAnchor,
      constant: -100
    )
        
    NSLayoutConstraint.activate([
      buttonBottomConstraint,
      floatingButton.trailingAnchor.constraint(
        equalTo: view.trailingAnchor,
        constant: -10
      ),
      floatingButton.heightAnchor.constraint(equalToConstant: 50),
      floatingButton.widthAnchor.constraint(equalToConstant: 50)
    ])
  }
  
  @objc
  private func openSheet() {
    let childViewController = ChildViewController()
    configureChildViewControllerFrameSubscription(childViewController)
    
    if let sheet = childViewController.sheetPresentationController {
      sheet.detents = [.small(), .medium(), .large()]
      sheet.largestUndimmedDetentIdentifier = .medium
      sheet.prefersGrabberVisible = true
    }
    
    childViewController.isModalInPresentation = true
    present(childViewController, animated: true)
  }
}

extension ViewController {
  private func configureChildViewControllerFrameSubscription(
    _ childViewController: ChildViewController
  ) {
    childViewController.framePassthroughSubject
      .receive(on: DispatchQueue.main)
      .sink { frame in
        self.buttonBottomConstraint.isActive = false
        
        self.buttonBottomConstraint = self.floatingButton.bottomAnchor.constraint(
          equalTo: self.view.bottomAnchor,
          constant: (frame.height * -1) - 10
        )
        
        self.buttonBottomConstraint.isActive = true
        self.floatingButton.layoutIfNeeded()
      }
      .store(in: &subscriptions)
  }
}

extension UISheetPresentationController.Detent.Identifier {
  static var smallIdentifier: Self {
    Self("small")
  }
}

extension UISheetPresentationController.Detent {
  static func small() -> UISheetPresentationController.Detent {
    .custom(identifier: .smallIdentifier) { context in
      context.maximumDetentValue * 0.15
    }
  }
}

class ChildViewController: UIViewController {
  let framePassthroughSubject = PassthroughSubject<CGRect, Never>()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    view.backgroundColor = .systemBlue
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    framePassthroughSubject.send(view.frame)
  }
}

内容的提问来源于stack exchange,提问作者rizz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 05:11:18