调用PKAddPaymentPassViewControllerDelegate的dismiss后PKAddPassButton移位求助
调用PKAddPaymentPassViewControllerDelegate的dismiss方法后,Flutter通过UIKitView展示的PKAddPassButton会自动移到屏幕顶部。具体场景为用户点击Apple Wallet的取消按钮,关闭Wallet应用后,该按钮发生移位,需求是关闭Wallet应用后按钮保持原位置不变。
Flutter端实现代码
UiKitView( viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: createParameters(state.panRefId), creationParamsCodec: const StandardMessageCodec(), ),
iOS端Swift实现代码
import UIKit import PassKit class WalletButtonController: UIViewController { let cardInfo: AppleWalletBankCardInfo public init(cardInfo:AppleWalletBankCardInfo) { self.cardInfo = cardInfo super.init(nibName: nil, bundle: nil) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupApplePayButton() } private func setupApplePayButton() { let passKitButton = PKAddPassButton(addPassButtonStyle: .blackOutline) passKitButton.frame = CGRect(x: 100, y: 100, width: 200, height: 50) passKitButton.addTarget(self, action: #selector(onEnroll), for: .touchUpInside) view.addSubview(passKitButton) passKitButton.translatesAutoresizingMaskIntoConstraints = false passKitButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16).isActive = true passKitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true passKitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true } @objc private func onEnroll(button: UIButton) { guard isPassKitAvailable() else { showPassKitUnavailable(message: "InApp enrollment not available for this device") return } initEnrollProcess() } func initEnrollProcess() { guard let configuration = PKAddPaymentPassRequestConfiguration(encryptionScheme: .ECC_V2) else { showPassKitUnavailable(message: "InApp enrollment configuraton fails") return } configuration.cardholderName = cardInfo.bankCardHolder configuration.primaryAccountSuffix = cardInfo.bankCardNumber configuration.localizedDescription = cardInfo.bankCardName configuration.primaryAccountIdentifier = cardInfo.bankCardPanRefId guard let enrollViewController = PKAddPaymentPassViewController(requestConfiguration: configuration, delegate: self) else { showPassKitUnavailable(message: "InApp enrollment controller configuration fails") return } enrollViewController.modalPresentationStyle = UIModalPresentationStyle.pageSheet; present(enrollViewController, animated: true, completion: nil) } private func isPassKitAvailable() -> Bool { return PKAddPaymentPassViewController.canAddPaymentPass() } private func showPassKitUnavailable(message: String) { let alert = UIAlertController(title: "InApp Error", message: message, preferredStyle: .alert) let action = UIAlertAction(title: "Ok", style: .default, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } } extension WalletButtonController: PKAddPaymentPassViewControllerDelegate { func addPaymentPassViewController( _ controller: PKAddPaymentPassViewController, generateRequestWithCertificateChain certificates: [Data], nonce: Data, nonceSignature: Data, completionHandler handler: @escaping (PKAddPaymentPassRequest) -> Void) { let certArray = certificates.map { $0.base64EncodedString() } let request = IssuerRequest(certificates: certArray, nonce: nonce.base64EncodedString(), nonceSignature: nonceSignature.base64EncodedString(), bankCardId: cardInfo.bankCardId, accessToken: cardInfo.accessToken,encryptUrl: cardInfo.encryptUrl) let interactor = GetPassKitDataIssuerHostInteractor() interactor.execute(request: request) { response in let request = PKAddPaymentPassRequest() request.activationData = Data(base64Encoded: response.activationData, options: .ignoreUnknownCharacters) request.ephemeralPublicKey = Data(base64Encoded: response.ephemeralPublicKey, options: .ignoreUnknownCharacters) request.encryptedPassData = Data(base64Encoded: response.encryptedPassData, options: .ignoreUnknownCharacters) handler(request) } } func addPaymentPassViewController( _ controller: PKAddPaymentPassViewController, didFinishAdding pass: PKPaymentPass?, error: Error?) { if let _ = pass { print("Bank card was successfully added to wallet") } self.dismiss(animated: true) } } private class GetPassKitDataIssuerHostInteractor { func execute(request: IssuerRequest, onComplete: @escaping(IssuerResponse) -> ()) { executeInternal(issuerRequest: request, completion: { (response) in let responseData = try response.get() let response = IssuerResponse(activationData: responseData.activationData, ephemeralPublicKey: responseData.ephemeralPublicKey, encryptedPassData: responseData.encryptedPassData) onComplete(response) }) } func executeInternal(issuerRequest: IssuerRequest ,completion: @escaping(Result<IssuerResponse, Error>) throws -> Void) { var request = URLRequest(url: URL(string:issuerRequest.encryptUrl)!) request.httpMethod = "POST" request.setValue("*/*", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(issuerRequest.accessToken)", forHTTPHeaderField: "Authorization") let body:[String:Any]=[ "bankCardId": "\(issuerRequest.bankCardId)", "nonce": "\(issuerRequest.nonce)", "nonceSignature": "\(issuerRequest.nonceSignature)", "certificates": issuerRequest.certificates ] request.httpBody = try? JSONSerialization.data(withJSONObject: body,options: []) URLSession.shared.dataTask(with: request) { (data, response, e) in if let httpResponse = response as? HTTPURLResponse{ if httpResponse.statusCode >= 400 { print(httpResponse.statusCode) print(response) return } } guard let data = data, e == nil else { print (e!.localizedDescription) return } do { let response = try JSONDecoder().decode(IssuerResponse.self, from: data) try completion(.success(response)) } catch { print (error.localizedDescription) return } }.resume() } } struct IssuerRequest { let certificates: [String] let nonce: String let nonceSignature: String let bankCardId: String let accessToken: String let encryptUrl: String } struct IssuerResponse : Decodable { let activationData: String let ephemeralPublicKey: String let encryptedPassData: String } struct AppleWalletBankCardInfo { let bankCardHolder: String let bankCardPanRefId: String let bankCardName: String let bankCardNumber: String let bankCardId: String let bankCardType: Int let accessToken: String let encryptUrl: String init(bankCardHolder: String, bankCardPanRefId: String, bankCardName: String, bankCardNumber: String, bankCardId: String, bankCardType: Int, accessToken: String,encryptUrl: String) { self.bankCardHolder = bankCardHolder self.bankCardPanRefId = bankCardPanRefId self.bankCardName = bankCardName self.bankCardNumber = bankCardNumber self.bankCardId = bankCardId self.bankCardType = bankCardType self.accessToken = accessToken self.encryptUrl = encryptUrl } }
解决方案
原因分析
移位问题大概率是因为dismiss操作后,safeAreaLayoutGuide的布局状态发生异常,导致Flutter与iOS原生视图的布局同步出现偏差,按钮的约束失效后自动回到屏幕顶部。
修改方案1:在dismiss完成后强制刷新布局
修改PKAddPaymentPassViewControllerDelegate中的didFinishAdding方法,在dismiss的completion回调中手动触发视图布局刷新:
func addPaymentPassViewController( _ controller: PKAddPaymentPassViewController, didFinishAdding pass: PKPaymentPass?, error: Error?) { if let _ = pass { print("Bank card was successfully added to wallet") } self.dismiss(animated: true) { // 强制刷新当前控制器视图的布局 self.view.layoutIfNeeded() // 如果仍有问题,可以尝试重新激活按钮约束 self.view.subviews.forEach { subview in if let button = subview as? PKAddPassButton { button.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 16).isActive = true } } } }
修改方案2:替换safeAreaLayoutGuide约束,手动计算顶部间距
在setupApplePayButton方法中,将依赖safeAreaLayoutGuide的约束改为直接使用view.topAnchor,并手动加上安全区域的顶部间距,避免safeArea状态变化影响布局:
private func setupApplePayButton() { let passKitButton = PKAddPassButton(addPassButtonStyle: .blackOutline) passKitButton.frame = CGRect(x: 100, y: 100, width: 200, height: 50) passKitButton.addTarget(self, action: #selector(onEnroll), for: .touchUpInside) view.addSubview(passKitButton) passKitButton.translatesAutoresizingMaskIntoConstraints = false // 手动计算安全区域顶部间距 let topInset = UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0 passKitButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 16 + topInset).isActive = true passKitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true passKitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true }
修改方案3:确保Flutter端UIKitView的布局约束正确
在Flutter代码中,给UIKitView添加明确的布局约束,比如用Container包裹并设置固定高度,或者用Expanded确保其在父布局中的位置稳定:
Container( height: 50, // 对应按钮的高度 padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: UiKitView( viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: createParameters(state.panRefId), creationParamsCodec: const StandardMessageCodec(), ), )
内容的提问来源于stack exchange,提问作者Software Engineer
相关产品推荐
相关产品推荐

