Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/Threema/SharedUI/Details/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 13 kB image not shown  

Quelle  EditProfilePictureView.swift   Sprache: JAVA

 

import ThreemaMacros
import UIKit

// MARK: - EditProfilePictureView.Configuration

extension EditProfilePictureView {
    private struct Configuration: DetailsConfiguration {
        /// Spacing between buttons at the bottom
        let buttonSpacing: CGFloat = 16
        
        /// Spacing between profile picture and buttons
        let verticalSpacing: CGFloat = 8
        
        let bottomSpacing: CGFloat = 16
        
        let profilePictureJPEGCompressionQuality: CGFloat = 0.7
    }
}

/// Show and change a provided profile picture
///
/// Changes are reported to the provided closure which needs to take care of storing the new image.
///
/// Changes are only possible if editing is allowed through the initializer.
/// You tell if the image is a default (and cannot be deleted) through the initializer and callback closure.
final class EditProfilePictureView: UIStackView {
    
    /// Closure called on every image change
    /// - Parameter newJPEGImageData: Cropped and resized JPEG image data of new chosen profile picture.
    ///                               `nil` if image was deleted.
    /// - Returns: An image for the new profile picture and if it is a default image (thus cannot be deleted)
    typealias ImageUpdated = (_ newJPEGImageData: Data?) -> (newProfilePicture: UIImage?, isDefaultImage: Bool)
    
    enum ConversationType {
        case contact, group, distributionList
    }
    
    // MARK: - Private properties
    
    /// View controller used to present the pickers on
    private weak var presentingViewController: UIViewController?

    private let conversationType: ConversationType
    
    private var profilePicture: UIImage? {
        didSet {
            guard profilePicture != oldValue else {
                return
            }
            updateProfileImageView()
        }
    }
    
    private let isEditable: Bool
    private var isDefaultImage: Bool {
        didSet {
            guard isDefaultImage != oldValue else {
                return
            }
            
            updateDeleteButton()
        }
    }
    
    private lazy var deleteAction: ThemedCodeButton.Action = { [weak self] _ in
        guard let strongSelf = self else {
            return
        }
        
        (strongSelf.profilePicture, strongSelf.isDefaultImage) = strongSelf.imageUpdated(nil)
    }
    
    private let imageUpdated: ImageUpdated
    
    private static let configuration = Configuration()
    
    // MARK: Subviews & layout
    
    private lazy var deleteButtonConstraints: [NSLayoutConstraint] = [
        deleteButton.topAnchor.constraint(equalTo: profilePictureView.topAnchor),
        deleteButton.trailingAnchor.constraint(equalTo: profilePictureView.trailingAnchor, constant: -8),
    ]

    private lazy var deleteButton = OpaqueDeleteButton(action: deleteAction)
    
    private lazy var profilePictureView: ProfilePictureImageView = {
        let profilePictureView = ProfilePictureImageView()
        profilePictureView.heightAnchor
            .constraint(equalToConstant: EditProfilePictureView.configuration.profilePictureSize).isActive = true
        
        profilePictureView.translatesAutoresizingMaskIntoConstraints = false
        return profilePictureView
    }()
    
    private lazy var takePictureButton = CircleButton(
        sfSymbolName: "camera",
        accessibilityLabel: #localize("take_photo")
    ) { [weak self] button in
        self?.showImagePicker(for: .camera, in: button)
    }
    
    private lazy var choosePictureButton = CircleButton(
        sfSymbolName: "photo.on.rectangle",
        accessibilityLabel: #localize("choose_existing_photo")
    ) { [weak self] button in
        self?.showImagePicker(for: .photoLibrary, in: button)
    }
    
    private lazy var buttonStack: UIStackView = {
        let stackView = UIStackView()
        
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            stackView.addArrangedSubview(takePictureButton)
        }
        
        if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
            stackView.addArrangedSubview(choosePictureButton)
        }
        
        stackView.axis = .horizontal
        stackView.spacing = EditProfilePictureView.configuration.buttonSpacing
        stackView.distribution = .equalSpacing
        
        return stackView
    }()
    
    // MARK: Accessibility actions
    
    // This should be in sync with the button stack
    private lazy var accessibilityDefaultActions: [UIAccessibilityCustomAction] = {
        var actions = [UIAccessibilityCustomAction]()
        
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            actions.append(UIAccessibilityCustomAction(
                name: #localize("take_photo"),
                target: self,
                selector: #selector(accessibilityShowCameraPicker)
            ))
        }
        
        if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
            actions.append(UIAccessibilityCustomAction(
                name: #localize("choose_existing_photo"),
                target: self,
                selector: #selector(accessibilityShowLibraryPicker)
            ))
        }
        
        return actions
    }()
    
    private lazy var accessibilityDeleteAction = UIAccessibilityCustomAction(
        name: #localize("delete"),
        target: self,
        selector: #selector(accessibilityDeleteProfilePicture)
    )
    
    // MARK: - Lifecycle
    
    /// Create a new view to edit the provided profile picture
    ///
    /// - Parameters:
    ///   - viewController: View controller to present the pickers on
    ///   - profilePicture: Profile picture to show initially
    ///   - isDefaultImage: Is the provided image a default, that cannot be deleted
    ///   - isEditable:     Can the profile picture actually be changed (including deletion)
    ///   - imageUpdated:   Closure called on every profile picture image change
    init(
        in viewController: UIViewController,
        profilePicture: UIImage?,
        isDefaultImage: Bool,
        isEditable: Bool = true,
        conversationType: ConversationType,
        imageUpdated: @escaping ImageUpdated
    ) {
        
        self.presentingViewController = viewController
        self.profilePicture = profilePicture
        self.isEditable = isEditable
        self.isDefaultImage = isDefaultImage
        self.imageUpdated = imageUpdated
        self.conversationType = conversationType
        
        super.init(frame: .zero)
        
        configureStack()
        updateProfileImageView()
        updateDeleteButton()
    }
    
    @available(*, unavailable)
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Configure
    
    private func configureStack() {
        // We initially hide the delete button to get a consistent state
        deleteButton.isHidden = true
        deleteButton.translatesAutoresizingMaskIntoConstraints = false
        
        profilePictureView.addSubview(deleteButton)
        addArrangedSubview(profilePictureView)

        if isEditable {
            addArrangedSubview(buttonStack)
        }
        
        axis = .vertical
        alignment = .center
        spacing = EditProfilePictureView.configuration.verticalSpacing
        
        directionalLayoutMargins = NSDirectionalEdgeInsets(
            top: 0,
            leading: 0,
            bottom: EditProfilePictureView.configuration.bottomSpacing,
            trailing: 0
        )
        isLayoutMarginsRelativeArrangement = true
        
        isAccessibilityElement = true
        
        if isEditable {
            accessibilityLabel = #localize("edit_avatar_edit_picture_accessibility_label")
        }
        else {
            accessibilityLabel = #localize("edit_avatar_picture_accessibility_label")
            accessibilityValue = #localize("edit_avatar_picture_not_editable_accessibility")
        }
    }
    
    // MARK: - Update
    
    private func updateProfileImageView() {
        guard let profilePicture else {
            switch conversationType {
            case .contact:
                profilePictureView.info = .edit(ProfilePictureGenerator.unknownContactImage)
            case .group:
                profilePictureView.info = .edit(ProfilePictureGenerator.unknownGroupImage)
            case .distributionList:
                profilePictureView.info = .edit(ProfilePictureGenerator.unknownDistributionListImage)
            }
            return
        }
        
        profilePictureView.info = .edit(profilePicture)
    }
    
    private func updateDeleteButton() {
        guard isEditable else {
            hideDeleteButton()
            return
        }
        
        if isDefaultImage {
            hideDeleteButton()
        }
        else {
            showDeleteButton()
        }
    }
    
    // MARK: Update helper
    
    private func hideDeleteButton() {
        guard !deleteButton.isHidden else {
            return
        }
        
        deleteButton.isHidden = true
        NSLayoutConstraint.deactivate(deleteButtonConstraints)
    }
    
    private func showDeleteButton() {
        guard deleteButton.isHidden else {
            return
        }
        
        deleteButton.isHidden = false
        NSLayoutConstraint.activate(deleteButtonConstraints)
    }
    
    // MARK: - Show picker helper
    
    private func showImagePicker(for sourceType: UIImagePickerController.SourceType, in view: UIView?) {
        guard let presentingViewController else {
            return
        }

        let vc = UIImagePickerController()
        vc.delegate = self
        vc.sourceType = sourceType
        vc.mediaTypes = [UTType.image.identifier]

        if sourceType == .photoLibrary, let view,
           presentingViewController.traitCollection.horizontalSizeClass == .regular {
            vc.modalPresentationStyle = .automatic
            let sourceRect = presentingViewController.view.convert(view.frame, from: view.superview)
            vc.popoverPresentationController?.sourceRect = sourceRect
            vc.popoverPresentationController?.sourceView = presentingViewController.view
        }

        presentingViewController.present(vc, animated: true)
    }
    
    // MARK: - Accessibility
    
    override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
        get {
            guard isEditable else {
                // No actions should be available if we cannot edit the profile picture
                return nil
            }
            
            var actions = accessibilityDefaultActions
            
            // Only add delete action if the button is also shown
            if !deleteButton.isHidden {
                actions.append(accessibilityDeleteAction)
            }
            
            return actions
        }
        set { }
    }
    
    // Helper for accessibility actions
    
    @objc private func accessibilityShowCameraPicker() -> Bool {
        showImagePicker(for: .camera, in: self)
        return true
    }
    
    @objc private func accessibilityShowLibraryPicker() -> Bool {
        showImagePicker(for: .photoLibrary, in: self)
        return true
    }
    
    @objc private func accessibilityDeleteProfilePicture() -> Bool {
        deleteAction(deleteButton)
        return true
    }
}

// MARK: - UINavigationControllerDelegate, UIImagePickerControllerDelegate

extension EditProfilePictureView: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    func imagePickerController(
        _ picker: UIImagePickerController,
        didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
    ) {
        guard let presentingViewController else {
            return
        }

        guard let selectedImage = info[.originalImage] as? UIImage else {
            picker.dismiss(animated: true) { [presentingViewController] in
                UIAlertTemplate.showOKAlert(
                    owner: presentingViewController,
                    title: #localize("edit_avatar_no_image_found_title"),
                    message: #localize("edit_avatar_no_image_found_message")
                )
            }
            return
        }
        
        let vc = RSKImageCropViewController(image: selectedImage, cropMode: .circle)
        vc.delegate = self
        vc.avoidEmptySpaceAroundImage = true
        // Make crop picker the appropriate size on an iPad
        let size = Int(kContactImageSize)
        vc.preferredContentSize = CGSize(width: size, height: size)
        
        picker.dismiss(animated: false) { [presentingViewController] in
            presentingViewController.present(vc, animated: true)
        }
    }
}

// MARK: - RSKImageCropViewControllerDelegate

extension EditProfilePictureView: RSKImageCropViewControllerDelegate {
    func imageCropViewController(
        _ controller: RSKImageCropViewController,
        didCropImage croppedImage: UIImage,
        usingCropRect cropRect: CGRect,
        rotationAngle: CGFloat
    ) {
    
        let scaledImage = MediaConverter.scale(croppedImage, toMaxSize: CGFloat(kContactImageSize))
        let jpegImageData = scaledImage?
            .jpegData(compressionQuality: EditProfilePictureView.configuration.profilePictureJPEGCompressionQuality)
        
        (profilePicture, isDefaultImage) = imageUpdated(jpegImageData)

        controller.dismiss(animated: true)
    }

    func imageCropViewControllerDidCancelCrop(_ controller: RSKImageCropViewController) {
        controller.dismiss(animated: true)
    }
}

Messung
CHZG
87969492

¤ Dauer der Verarbeitung: 0.6 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.