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

调用PKAddPaymentPassViewControllerDelegate的dismiss后PKAddPassButton移位求助

问题:关闭PKAddPaymentPassViewController后Flutter UIKitView中的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 07:09:49