Various fixes

This commit is contained in:
Ilya Laktyushin 2023-12-01 15:18:45 +04:00
parent 271682488a
commit 87bab82fa0
15 changed files with 460 additions and 71 deletions

View File

@ -50,11 +50,11 @@ final class CameraDeviceContext {
let input = CameraInput()
let output: CameraOutput
init(session: CameraSession, exclusive: Bool, additional: Bool) {
init(session: CameraSession, exclusive: Bool, additional: Bool, ciContext: CIContext) {
self.session = session
self.exclusive = exclusive
self.additional = additional
self.output = CameraOutput(exclusive: exclusive)
self.output = CameraOutput(exclusive: exclusive, ciContext: ciContext)
}
func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool, preferWide: Bool = false, preferLowerFramerate: Bool = false) {
@ -114,7 +114,7 @@ private final class CameraContext {
private var mainDeviceContext: CameraDeviceContext?
private var additionalDeviceContext: CameraDeviceContext?
private let cameraImageContext = CIContext()
private let ciContext = CIContext()
private let initialConfiguration: Camera.Configuration
private var invalidated = false
@ -140,7 +140,7 @@ private final class CameraContext {
ciImage = ciImage.transformed(by: transform)
}
ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size))
if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) {
if let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) {
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
if front {
CameraSimplePreviewView.saveLastFrontImage(uiImage)
@ -303,10 +303,10 @@ private final class CameraContext {
if enabled {
self.configure {
self.mainDeviceContext?.invalidate()
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: false)
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: false, ciContext: self.ciContext)
self.mainDeviceContext?.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: true)
self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: true, ciContext: self.ciContext)
self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false)
}
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
@ -343,7 +343,7 @@ private final class CameraContext {
self.additionalDeviceContext?.invalidate()
self.additionalDeviceContext = nil
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true, additional: false)
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true, additional: false, ciContext: self.ciContext)
self.mainDeviceContext?.configure(position: self.positionValue, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata, preferWide: self.initialConfiguration.preferWide, preferLowerFramerate: self.initialConfiguration.preferLowerFramerate)
}
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in

View File

@ -78,13 +78,14 @@ public struct CameraCode: Equatable {
}
final class CameraOutput: NSObject {
let exclusive: Bool
let ciContext: CIContext
let photoOutput = AVCapturePhotoOutput()
let videoOutput = AVCaptureVideoDataOutput()
let audioOutput = AVCaptureAudioDataOutput()
let metadataOutput = AVCaptureMetadataOutput()
let exclusive: Bool
private var photoConnection: AVCaptureConnection?
private var videoConnection: AVCaptureConnection?
private var previewConnection: AVCaptureConnection?
@ -99,8 +100,9 @@ final class CameraOutput: NSObject {
var processAudioBuffer: ((CMSampleBuffer) -> Void)?
var processCodes: (([CameraCode]) -> Void)?
init(exclusive: Bool) {
init(exclusive: Bool, ciContext: CIContext) {
self.exclusive = exclusive
self.ciContext = ciContext
super.init()
@ -266,7 +268,7 @@ final class CameraOutput: NSObject {
}
let uniqueId = settings.uniqueID
let photoCapture = PhotoCaptureContext(settings: settings, orientation: orientation, mirror: mirror)
let photoCapture = PhotoCaptureContext(ciContext: self.ciContext, settings: settings, orientation: orientation, mirror: mirror)
self.photoCaptureRequests[uniqueId] = photoCapture
self.photoOutput.capturePhoto(with: settings, delegate: photoCapture)
@ -309,7 +311,7 @@ final class CameraOutput: NSObject {
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
let outputFileURL = URL(fileURLWithPath: outputFilePath)
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), orientation: orientation, fileUrl: outputFileURL, completion: { [weak self] result in
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), ciContext: self.ciContext, orientation: orientation, fileUrl: outputFileURL, completion: { [weak self] result in
guard let self else {
return
}

View File

@ -33,11 +33,13 @@ public enum PhotoCaptureResult: Equatable {
}
final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate {
private let ciContext: CIContext
private let pipe = ValuePipe<PhotoCaptureResult>()
private let orientation: AVCaptureVideoOrientation
private let mirror: Bool
init(settings: AVCapturePhotoSettings, orientation: AVCaptureVideoOrientation, mirror: Bool) {
init(ciContext: CIContext, settings: AVCapturePhotoSettings, orientation: AVCaptureVideoOrientation, mirror: Bool) {
self.ciContext = ciContext
self.orientation = orientation
self.mirror = mirror
@ -70,9 +72,8 @@ final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate {
}
let finalPixelBuffer = photoPixelBuffer
let ciContext = CIContext()
let renderedCIImage = CIImage(cvImageBuffer: finalPixelBuffer)
if let cgImage = ciContext.createCGImage(renderedCIImage, from: renderedCIImage.extent) {
if let cgImage = self.ciContext.createCGImage(renderedCIImage, from: renderedCIImage.extent) {
var image = UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
if image.imageOrientation != .up {
UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale)

View File

@ -34,7 +34,7 @@ private final class VideoRecorderImpl {
private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput?
private let imageContext = CIContext()
private let ciContext: CIContext
private var transitionImage: UIImage?
private var savedTransitionImage = false
@ -63,8 +63,9 @@ private final class VideoRecorderImpl {
private var hasAllVideoBuffers = false
private var hasAllAudioBuffers = false
public init?(configuration: VideoRecorder.Configuration, orientation: AVCaptureVideoOrientation, fileUrl: URL) {
public init?(configuration: VideoRecorder.Configuration, ciContext: CIContext, orientation: AVCaptureVideoOrientation, fileUrl: URL) {
self.configuration = configuration
self.ciContext = ciContext
var transform: CGAffineTransform = CGAffineTransform(rotationAngle: .pi / 2.0)
if orientation == .landscapeLeft {
@ -184,7 +185,7 @@ private final class VideoRecorderImpl {
self.savedTransitionImage = true
Queue.concurrentBackgroundQueue().async {
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
if let cgImage = self.imageContext.createCGImage(ciImage, from: ciImage.extent) {
if let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) {
var orientation: UIImage.Orientation = .right
if self.orientation == .landscapeLeft {
orientation = .down
@ -479,12 +480,12 @@ public final class VideoRecorder {
return self.impl.isRecording
}
init?(configuration: Configuration, orientation: AVCaptureVideoOrientation, fileUrl: URL, completion: @escaping (Result) -> Void) {
init?(configuration: Configuration, ciContext: CIContext, orientation: AVCaptureVideoOrientation, fileUrl: URL, completion: @escaping (Result) -> Void) {
self.configuration = configuration
self.fileUrl = fileUrl
self.completion = completion
guard let impl = VideoRecorderImpl(configuration: configuration, orientation: orientation, fileUrl: fileUrl) else {
guard let impl = VideoRecorderImpl(configuration: configuration, ciContext: ciContext, orientation: orientation, fileUrl: fileUrl) else {
completion(.initError(.generic))
return nil
}

View File

@ -3082,12 +3082,15 @@ public final class DrawingToolsInteraction {
return
}
var isRectangleImage = false
var isVideo = false
var isAdditional = false
if let entity = entityView.entity as? DrawingStickerEntity {
if case let .dualVideoReference(isAdditionalValue) = entity.content {
isVideo = true
isAdditional = isAdditionalValue
} else if case let .image(_, type) = entity.content, case .rectangle = type {
isRectangleImage = true
}
}
@ -3144,6 +3147,21 @@ public final class DrawingToolsInteraction {
}))
}
}
#if DEBUG
if isRectangleImage {
actions.append(ContextMenuAction(content: .text(title: "Cut Out", accessibilityLabel: "Cut Out"), action: { [weak self, weak entityView] in
if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content {
let _ = (cutoutStickerImage(from: image)
|> deliverOnMainQueue).start(next: { result in
if let result {
let newEntity = DrawingStickerEntity(content: .image(result, .sticker))
self.insertEntity(newEntity)
}
})
}
}))
}
#endif
let entityFrame = entityView.convert(entityView.selectionBounds, to: node.view).offsetBy(dx: 0.0, dy: -6.0)
let controller = makeContextMenuController(actions: actions)
let bounds = node.bounds.insetBy(dx: 0.0, dy: 160.0)

View File

@ -0,0 +1,74 @@
import Foundation
import UIKit
import Vision
import CoreImage
import SwiftSignalKit
import VideoToolbox
private let queue = Queue()
func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError> {
if #available(iOS 17.0, *) {
guard let cgImage = image.cgImage else {
return .single(nil)
}
return Signal { subscriber in
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in
guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else {
subscriber.putNext(nil)
subscriber.putCompletion()
return
}
let instances = instances(atPoint: nil, inObservation: result)
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler), let image = UIImage(pixelBuffer: mask) {
subscriber.putNext(image)
subscriber.putCompletion()
} else {
subscriber.putNext(nil)
subscriber.putCompletion()
}
}
try? handler.perform([request])
return ActionDisposable {
request.cancel()
}
}
|> runOn(queue)
} else {
return .single(nil)
}
}
@available(iOS 17.0, *)
private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet {
guard let point = maybePoint else {
return observation.allInstances
}
let instanceMap = observation.instanceMask
let coords = VNImagePointForNormalizedPoint(point, CVPixelBufferGetWidth(instanceMap) - 1, CVPixelBufferGetHeight(instanceMap) - 1)
CVPixelBufferLockBaseAddress(instanceMap, .readOnly)
guard let pixels = CVPixelBufferGetBaseAddress(instanceMap) else {
fatalError()
}
let bytesPerRow = CVPixelBufferGetBytesPerRow(instanceMap)
let instanceLabel = pixels.load(fromByteOffset: Int(coords.y) * bytesPerRow + Int(coords.x), as: UInt8.self)
CVPixelBufferUnlockBaseAddress(instanceMap, .readOnly)
return instanceLabel == 0 ? observation.allInstances : [Int(instanceLabel)]
}
private extension UIImage {
convenience init?(pixelBuffer: CVPixelBuffer) {
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
guard let cgImage = cgImage else {
return nil
}
self.init(cgImage: cgImage)
}
}

View File

@ -110,6 +110,8 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
private var items: [Item] = []
private var lastTimeStep: Double = 0.0
public var animationSpeed: Float = 1.0
public var becameEmpty: (() -> Void)?
override public init() {
@ -164,7 +166,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
var didRemoveItems = false
for i in (0 ..< self.items.count).reversed() {
self.items[i].phase += Float(deltaTimeValue) / Float(UIView.animationDurationFactor())
self.items[i].phase += Float(deltaTimeValue) * self.animationSpeed / Float(UIView.animationDurationFactor())
if self.items[i].phase >= 4.0 {
self.items.remove(at: i)

View File

@ -47,7 +47,8 @@ swift_library(
"//submodules/TelegramUI/Components/AudioWaveformComponent",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/VolumeSliderContextItem",
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent"
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -38,11 +38,13 @@ import ReactionSelectionNode
import VolumeSliderContextItem
import TelegramStringFormatting
import ForwardInfoPanelComponent
import ContextReferenceButtonComponent
private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
private let saveButtonTag = GenericComponentViewTag()
private let switchCameraButtonTag = GenericComponentViewTag()
private let stickerButtonTag = GenericComponentViewTag()
final class MediaEditorScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -870,19 +872,25 @@ final class MediaEditorScreenComponent: Component {
let stickerButtonSize = self.stickerButton.update(
transition: transition,
component: AnyComponent(Button(
component: AnyComponent(ContextReferenceButtonComponent(
content: AnyComponent(Image(
image: state.image(.sticker),
size: CGSize(width: 30.0, height: 30.0)
)),
action: { [weak self] in
tag: stickerButtonTag,
minSize: CGSize(width: 30.0, height: 30.0),
action: { [weak self] view, gesture in
guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else {
return
}
guard !controller.node.recording.isActive else {
return
}
openDrawing(.sticker)
if let gesture {
controller.presentEntityShortcuts(sourceView: view, gesture: gesture)
} else {
openDrawing(.sticker)
}
}
)),
environment: {},
@ -1169,17 +1177,7 @@ final class MediaEditorScreenComponent: Component {
guard let controller else {
return
}
let context = controller.context
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak controller] peer in
let hasPremium: Bool
if case let .user(user) = peer {
hasPremium = user.isPremium
} else {
hasPremium = false
}
controller?.presentTimeoutSetup(sourceView: view, gesture: gesture, hasPremium: hasPremium)
})
controller.presentTimeoutSetup(sourceView: view, gesture: gesture)
},
forwardAction: nil,
moreAction: nil,
@ -3514,6 +3512,33 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.controller?.present(contextController, in: .window(.root))
}
func addReaction() {
guard let controller = self.controller else {
return
}
let maxReactionCount = self.context.userLimits.maxStoriesSuggestedReactions
var currentReactionCount = 0
self.entitiesView.eachView { entityView in
if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type {
currentReactionCount += 1
}
}
if currentReactionCount >= maxReactionCount {
controller.presentReactionPremiumSuggestion()
return
}
let heart = "❤️".strippedEmoji
if let reaction = self.availableReactions.first(where: { reaction in
return reaction.reaction.rawValue == .builtin(heart)
}) {
let stickerEntity = DrawingStickerEntity(content: .file(reaction.stillAnimation, .reaction(.builtin(heart), .white)))
self.interaction?.insertEntity(stickerEntity, scale: 1.175)
}
self.mediaEditor?.play()
}
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
return
@ -3703,30 +3728,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
controller.addReaction = { [weak self, weak controller] in
if let self {
let maxReactionCount = self.context.userLimits.maxStoriesSuggestedReactions
var currentReactionCount = 0
self.entitiesView.eachView { entityView in
if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type {
currentReactionCount += 1
}
}
if currentReactionCount >= maxReactionCount {
self.controller?.presentReactionPremiumSuggestion()
return
}
self.addReaction()
self.stickerScreen = nil
controller?.dismiss(animated: true)
let heart = "❤️".strippedEmoji
if let reaction = self.availableReactions.first(where: { reaction in
return reaction.reaction.rawValue == .builtin(heart)
}) {
let stickerEntity = DrawingStickerEntity(content: .file(reaction.stillAnimation, .reaction(.builtin(heart), .white)))
self.interaction?.insertEntity(stickerEntity, scale: 1.175)
}
self.mediaEditor?.play()
}
}
controller.pushController = { [weak self] c in
@ -4401,11 +4406,55 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
})
}
func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?, hasPremium: Bool) {
func presentEntityShortcuts(sourceView: UIView, gesture: ContextGesture) {
self.hapticFeedback.impact(.light)
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Image", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Image"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
self?.node.presentGallery()
})))
items.append(.action(ContextMenuActionItem(text: "Location", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LocationSmall"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
self?.node.presentLocationPicker()
})))
items.append(.action(ContextMenuActionItem(text: "Reaction", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
self?.node.addReaction()
})))
items.append(.action(ContextMenuActionItem(text: "Audio", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AudioSmall"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
self?.node.presentAudioPicker()
})))
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.present(contextController, in: .window(.root))
}
func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) {
self.hapticFeedback.impact(.light)
let hasPremium = self.context.isPremium
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let title = presentationData.strings.Story_Editor_ExpirationText
let currentValue = self.state.privacy.timeout
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
let updateTimeout: (Int?) -> Void = { [weak self] timeout in
guard let self else {
return
@ -4418,12 +4467,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
pin: self.state.privacy.pin
)
}
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let title = presentationData.strings.Story_Editor_ExpirationText
let currentValue = self.state.privacy.timeout
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(6), icon: { theme in

View File

@ -170,6 +170,12 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
}
return nil
})
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
if let self {
self.layoutUnlockPanel(transition: .animated(duration: 0.4, curve: .spring))
}
}
}
deinit {
@ -233,18 +239,18 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
}, openPeerContextAction: { [weak self] peer, node, gesture in
self?.openPeerContextAction(true, peer, node, gesture)
})
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
self.layoutUnlockPanel()
}
private func layoutUnlockPanel() {
private func layoutUnlockPanel(transition: ContainedViewLayoutTransition) {
guard let (_, isPremium) = self.currentState, let currentParams = self.currentParams else {
return
}
if !isPremium {
var transition = transition
let size = currentParams.size
let sideInset = currentParams.sideInset
let bottomInset = currentParams.bottomInset
@ -261,6 +267,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
} else {
unlockText = ComponentView<Empty>()
self.unlockText = unlockText
transition = .immediate
}
if let current = self.unlockBackground {
@ -327,14 +334,14 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.unlockPressed)))
self.view.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: size.height - bottomInset - unlockSize.height - 13.0 + scrollOffset), size: unlockSize)
transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: size.height - bottomInset - unlockSize.height - 13.0 + scrollOffset), size: unlockSize))
}
unlockBackground.frame = CGRect(x: 0.0, y: size.height - bottomInset - 170.0 + scrollOffset, width: size.width, height: bottomInset + 170.0)
transition.updateFrame(view: unlockBackground, frame: CGRect(x: 0.0, y: size.height - bottomInset - 170.0 + scrollOffset, width: size.width, height: bottomInset + 170.0))
let buttonSideInset = sideInset + 16.0
let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0)
unlockButton.frame = CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - unlockSize.height - buttonSize.height - 26.0 + scrollOffset), size: buttonSize)
transition.updateFrame(node: unlockButton, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - unlockSize.height - buttonSize.height - 26.0 + scrollOffset), size: buttonSize))
let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate)
} else {
self.unlockBackground?.removeFromSuperview()

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Type=Add.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,119 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.334961 1.926331 cm
1.000000 1.000000 1.000000 scn
19.499893 17.264488 m
19.499886 17.485376 19.289846 17.645796 19.076740 17.587671 c
15.576847 16.633081 l
15.431101 16.593330 15.329992 16.460947 15.329997 16.309877 c
15.330069 13.944344 l
19.253101 15.014343 l
19.398848 15.054096 19.499956 15.186479 19.499950 15.337549 c
19.499893 17.264488 l
h
14.000095 13.073683 m
13.999997 16.309837 l
13.999974 17.060678 14.502496 17.718637 15.226875 17.916212 c
18.726768 18.870800 l
19.785927 19.159685 20.829859 18.362379 20.829891 17.264528 c
20.829950 15.337589 l
20.829973 14.586750 20.327454 13.928788 19.603073 13.731215 c
15.330095 12.565768 l
15.330095 5.073745 l
15.330057 5.073745 l
15.330061 4.174320 l
15.330069 2.634279 14.299319 1.284771 12.813541 0.879566 c
12.386425 0.763084 l
10.429088 0.229277 8.499945 1.702747 8.499934 3.731570 c
8.499928 5.119122 9.428616 6.335007 10.767277 6.700088 c
14.000095 7.581746 l
14.000095 13.073662 l
14.000095 13.073683 l
h
14.000051 6.203160 m
11.117216 5.416950 l
10.357193 5.209676 9.829930 4.519358 9.829934 3.731577 c
9.829940 2.579716 10.925209 1.743153 12.036487 2.046223 c
12.463603 2.162704 l
13.370743 2.410101 14.000067 3.234043 14.000061 4.174314 c
14.000051 6.203160 l
h
4.665000 17.738724 m
5.032269 17.738724 5.330000 17.440992 5.330000 17.073723 c
5.330000 13.738724 l
8.665000 13.738724 l
9.032269 13.738724 9.330000 13.440992 9.330000 13.073723 c
9.330000 12.706453 9.032269 12.408724 8.665000 12.408724 c
5.330000 12.408724 l
5.330000 9.073723 l
5.330000 8.706453 5.032269 8.408724 4.665000 8.408724 c
4.297730 8.408724 4.000000 8.706453 4.000000 9.073723 c
4.000000 12.408724 l
0.665000 12.408724 l
0.297731 12.408724 0.000000 12.706453 0.000000 13.073723 c
0.000000 13.440992 0.297731 13.738724 0.665000 13.738724 c
4.000000 13.738724 l
4.000000 17.073723 l
4.000000 17.440992 4.297730 17.738724 4.665000 17.738724 c
h
f*
n
Q
endstream
endobj
3 0 obj
1960
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002050 00000 n
0000002073 00000 n
0000002246 00000 n
0000002320 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2379
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "location_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,95 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.584961 2.334961 cm
0.000000 0.000000 0.000000 scn
1.330000 11.915017 m
1.330000 15.275670 4.054347 18.000017 7.415000 18.000017 c
10.775653 18.000017 13.500000 15.275670 13.500000 11.915017 c
13.500000 9.671107 12.396618 6.987004 10.997435 4.832344 c
10.303381 3.763542 9.557351 2.857718 8.878304 2.227985 c
8.538435 1.912800 8.228340 1.678921 7.961973 1.527348 c
7.686846 1.370789 7.507371 1.330017 7.415000 1.330017 c
7.322628 1.330017 7.143155 1.370789 6.868028 1.527348 c
6.601661 1.678921 6.291565 1.912800 5.951696 2.227985 c
5.272649 2.857718 4.526619 3.763542 3.832566 4.832344 c
2.433383 6.987004 1.330000 9.671107 1.330000 11.915017 c
h
7.415000 19.330017 m
3.319808 19.330017 0.000000 16.010208 0.000000 11.915017 c
0.000000 9.306005 1.251407 6.365108 2.717118 4.107999 c
3.455511 2.970917 4.269455 1.974163 5.047323 1.252789 c
5.435911 0.892422 5.828508 0.588619 6.210246 0.371395 c
6.583224 0.159157 6.997266 0.000019 7.415000 0.000019 c
7.832734 0.000019 8.246777 0.159157 8.619755 0.371395 c
9.001492 0.588619 9.394089 0.892422 9.782678 1.252789 c
10.560545 1.974163 11.374489 2.970917 12.112883 4.107999 c
13.578593 6.365108 14.830000 9.306005 14.830000 11.915017 c
14.830000 16.010208 11.510192 19.330017 7.415000 19.330017 c
h
7.415049 9.215029 m
8.906218 9.215029 10.115049 10.423861 10.115049 11.915030 c
10.115049 13.406199 8.906218 14.615029 7.415049 14.615029 c
5.923880 14.615029 4.715049 13.406199 4.715049 11.915030 c
4.715049 10.423861 5.923880 9.215029 7.415049 9.215029 c
h
f*
n
Q
endstream
endobj
3 0 obj
1579
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001669 00000 n
0000001692 00000 n
0000001865 00000 n
0000001939 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1998
%%EOF