mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-04 10:30:42 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
5d5a5bbf83
@ -1 +1 @@
|
||||
4f0d2d13a70664d3029d9b97935089df0426fe53745965d175408752838b80dd
|
||||
61c0e29ede9b63175583b4609216b9c6083192c87d0e6ee0a42a5ff263b627dc
|
||||
|
@ -4,8 +4,8 @@ set -e
|
||||
|
||||
BUILD_TELEGRAM_VERSION="1"
|
||||
|
||||
MACOS_VERSION="10.15"
|
||||
XCODE_VERSION="12.4"
|
||||
MACOS_VERSION="11"
|
||||
XCODE_VERSION="12.5.1"
|
||||
GUEST_SHELL="bash"
|
||||
|
||||
VM_BASE_NAME="macos$(echo $MACOS_VERSION | sed -e 's/\.'/_/g)_Xcode$(echo $XCODE_VERSION | sed -e 's/\.'/_/g)"
|
||||
|
@ -130,7 +130,7 @@ public enum ChatControllerInteractionNavigateToPeer {
|
||||
case withBotStartPayload(ChatControllerInitialBotStart)
|
||||
}
|
||||
|
||||
public struct ChatTextInputState: PostboxCoding, Equatable {
|
||||
public struct ChatTextInputState: Codable, Equatable {
|
||||
public let inputText: NSAttributedString
|
||||
public let selectionRange: Range<Int>
|
||||
|
||||
@ -153,29 +153,40 @@ public struct ChatTextInputState: PostboxCoding, Equatable {
|
||||
let length = inputText.length
|
||||
self.selectionRange = length ..< length
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.inputText = ((decoder.decodeObjectForKey("at", decoder: { ChatTextInputStateText(decoder: $0) }) as? ChatTextInputStateText) ?? ChatTextInputStateText()).attributedText()
|
||||
self.selectionRange = Int(decoder.decodeInt32ForKey("as0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("as1", orElse: 0))
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
self.inputText = ((try? container.decode(ChatTextInputStateText.self, forKey: "at")) ?? ChatTextInputStateText()).attributedText()
|
||||
let rangeFrom = (try? container.decode(Int32.self, forKey: "as0")) ?? 0
|
||||
let rangeTo = (try? container.decode(Int32.self, forKey: "as1")) ?? 0
|
||||
if rangeFrom <= rangeTo {
|
||||
self.selectionRange = Int(rangeFrom) ..< Int(rangeTo)
|
||||
} else {
|
||||
let length = self.inputText.length
|
||||
self.selectionRange = length ..< length
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(ChatTextInputStateText(attributedText: self.inputText), forKey: "at")
|
||||
|
||||
encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "as0")
|
||||
encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "as1")
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode(ChatTextInputStateText(attributedText: self.inputText), forKey: "at")
|
||||
try container.encode(Int32(self.selectionRange.lowerBound), forKey: "as0")
|
||||
try container.encode(Int32(self.selectionRange.upperBound), forKey: "as1")
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatTextInputStateTextAttributeType: PostboxCoding, Equatable {
|
||||
public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
||||
case bold
|
||||
case italic
|
||||
case monospace
|
||||
case textMention(EnginePeer.Id)
|
||||
case textUrl(String)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("t", orElse: 0) {
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
switch (try? container.decode(Int32.self, forKey: "t")) ?? 0 {
|
||||
case 0:
|
||||
self = .bold
|
||||
case 1:
|
||||
@ -183,69 +194,37 @@ public enum ChatTextInputStateTextAttributeType: PostboxCoding, Equatable {
|
||||
case 2:
|
||||
self = .monospace
|
||||
case 3:
|
||||
self = .textMention(EnginePeer.Id(decoder.decodeInt64ForKey("peerId", orElse: 0)))
|
||||
let peerId = (try? container.decode(Int64.self, forKey: "peerId")) ?? 0
|
||||
self = .textMention(EnginePeer.Id(peerId))
|
||||
case 4:
|
||||
self = .textUrl(decoder.decodeStringForKey("url", orElse: ""))
|
||||
let url = (try? container.decode(String.self, forKey: "url")) ?? ""
|
||||
self = .textUrl(url)
|
||||
default:
|
||||
assertionFailure()
|
||||
self = .bold
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
switch self {
|
||||
case .bold:
|
||||
encoder.encodeInt32(0, forKey: "t")
|
||||
try container.encode(0 as Int32, forKey: "t")
|
||||
case .italic:
|
||||
encoder.encodeInt32(1, forKey: "t")
|
||||
try container.encode(1 as Int32, forKey: "t")
|
||||
case .monospace:
|
||||
encoder.encodeInt32(2, forKey: "t")
|
||||
try container.encode(2 as Int32, forKey: "t")
|
||||
case let .textMention(id):
|
||||
encoder.encodeInt32(3, forKey: "t")
|
||||
encoder.encodeInt64(id.toInt64(), forKey: "peerId")
|
||||
try container.encode(3 as Int32, forKey: "t")
|
||||
try container.encode(id.toInt64(), forKey: "peerId")
|
||||
case let .textUrl(url):
|
||||
encoder.encodeInt32(4, forKey: "t")
|
||||
encoder.encodeString(url, forKey: "url")
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatTextInputStateTextAttributeType, rhs: ChatTextInputStateTextAttributeType) -> Bool {
|
||||
switch lhs {
|
||||
case .bold:
|
||||
if case .bold = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .italic:
|
||||
if case .italic = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .monospace:
|
||||
if case .monospace = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .textMention(id):
|
||||
if case .textMention(id) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .textUrl(url):
|
||||
if case .textUrl(url) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
try container.encode(4 as Int32, forKey: "t")
|
||||
try container.encode(url, forKey: "url")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatTextInputStateTextAttribute: PostboxCoding, Equatable {
|
||||
public struct ChatTextInputStateTextAttribute: Codable, Equatable {
|
||||
public let type: ChatTextInputStateTextAttributeType
|
||||
public let range: Range<Int>
|
||||
|
||||
@ -253,16 +232,23 @@ public struct ChatTextInputStateTextAttribute: PostboxCoding, Equatable {
|
||||
self.type = type
|
||||
self.range = range
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.type = decoder.decodeObjectForKey("type", decoder: { ChatTextInputStateTextAttributeType(decoder: $0) }) as! ChatTextInputStateTextAttributeType
|
||||
self.range = Int(decoder.decodeInt32ForKey("range0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("range1", orElse: 0))
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
self.type = try container.decode(ChatTextInputStateTextAttributeType.self, forKey: "type")
|
||||
let rangeFrom = (try? container.decode(Int32.self, forKey: "range0")) ?? 0
|
||||
let rangeTo = (try? container.decode(Int32.self, forKey: "range1")) ?? 0
|
||||
|
||||
self.range = Int(rangeFrom) ..< Int(rangeTo)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.type, forKey: "type")
|
||||
encoder.encodeInt32(Int32(self.range.lowerBound), forKey: "range0")
|
||||
encoder.encodeInt32(Int32(self.range.upperBound), forKey: "range1")
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode(self.type, forKey: "type")
|
||||
|
||||
try container.encode(Int32(self.range.lowerBound), forKey: "range0")
|
||||
try container.encode(Int32(self.range.upperBound), forKey: "range1")
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatTextInputStateTextAttribute, rhs: ChatTextInputStateTextAttribute) -> Bool {
|
||||
@ -270,7 +256,7 @@ public struct ChatTextInputStateTextAttribute: PostboxCoding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatTextInputStateText: PostboxCoding, Equatable {
|
||||
public struct ChatTextInputStateText: Codable, Equatable {
|
||||
public let text: String
|
||||
public let attributes: [ChatTextInputStateTextAttribute]
|
||||
|
||||
@ -304,15 +290,17 @@ public struct ChatTextInputStateText: PostboxCoding, Equatable {
|
||||
})
|
||||
self.attributes = parsedAttributes
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.text = decoder.decodeStringForKey("text", orElse: "")
|
||||
self.attributes = decoder.decodeObjectArrayWithDecoderForKey("attributes")
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
self.text = (try? container.decode(String.self, forKey: "text")) ?? ""
|
||||
self.attributes = (try? container.decode([ChatTextInputStateTextAttribute].self, forKey: "attributes")) ?? []
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeString(self.text, forKey: "text")
|
||||
encoder.encodeObjectArray(self.attributes, forKey: "attributes")
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
try container.encode(self.text, forKey: "text")
|
||||
try container.encode(self.attributes, forKey: "attributes")
|
||||
}
|
||||
|
||||
static public func ==(lhs: ChatTextInputStateText, rhs: ChatTextInputStateText) -> Bool {
|
||||
@ -359,15 +347,18 @@ public final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceStat
|
||||
self.timestamp = timestamp
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.timestamp = decoder.decodeInt32ForKey("d", orElse: 0)
|
||||
self.text = ((decoder.decodeObjectForKey("at", decoder: { ChatTextInputStateText(decoder: $0) }) as? ChatTextInputStateText) ?? ChatTextInputStateText()).attributedText()
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
self.timestamp = (try? container.decode(Int32.self, forKey: "d")) ?? 0
|
||||
self.text = ((try? container.decode(ChatTextInputStateText.self, forKey: "at")) ?? ChatTextInputStateText()).attributedText()
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.timestamp, forKey: "d")
|
||||
encoder.encodeObject(ChatTextInputStateText(attributedText: self.text), forKey: "at")
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode(self.timestamp, forKey: "d")
|
||||
try container.encode(ChatTextInputStateText(attributedText: self.text), forKey: "at")
|
||||
}
|
||||
|
||||
public func isEqual(to: PeerChatListEmbeddedInterfaceState) -> Bool {
|
||||
|
@ -10,7 +10,7 @@ public enum ChatTextInputMediaRecordingButtonMode: Int32 {
|
||||
case video = 1
|
||||
}
|
||||
|
||||
public struct ChatInterfaceSelectionState: PostboxCoding, Equatable {
|
||||
public struct ChatInterfaceSelectionState: Codable, Equatable {
|
||||
public let selectedIds: Set<MessageId>
|
||||
|
||||
public static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool {
|
||||
@ -20,8 +20,28 @@ public struct ChatInterfaceSelectionState: PostboxCoding, Equatable {
|
||||
public init(selectedIds: Set<MessageId>) {
|
||||
self.selectedIds = selectedIds
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
if let data = try? container.decodeIfPresent(Data.self, forKey: "i") {
|
||||
self.selectedIds = Set(MessageId.decodeArrayFromBuffer(ReadBuffer(data: data)))
|
||||
} else {
|
||||
self.selectedIds = Set()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
let buffer = WriteBuffer()
|
||||
MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer)
|
||||
|
||||
try container.encode(buffer.makeData(), forKey: "i")
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
/*public init(decoder: PostboxDecoder) {
|
||||
if let data = decoder.decodeBytesForKeyNoCopy("i") {
|
||||
self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data))
|
||||
} else {
|
||||
@ -33,10 +53,10 @@ public struct ChatInterfaceSelectionState: PostboxCoding, Equatable {
|
||||
let buffer = WriteBuffer()
|
||||
MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer)
|
||||
encoder.encodeBytes(buffer, forKey: "i")
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
public struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
public struct ChatEditMessageState: Codable, Equatable {
|
||||
public let messageId: MessageId
|
||||
public let inputState: ChatTextInputState
|
||||
public let disableUrlPreview: String?
|
||||
@ -48,8 +68,41 @@ public struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
self.disableUrlPreview = disableUrlPreview
|
||||
self.inputTextMaxLength = inputTextMaxLength
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
self.messageId = MessageId(
|
||||
peerId: PeerId((try? container.decode(Int64.self, forKey: "mp")) ?? 0),
|
||||
namespace: (try? container.decode(Int32.self, forKey: "mn")) ?? 0,
|
||||
id: (try? container.decode(Int32.self, forKey: "mi")) ?? 0
|
||||
)
|
||||
|
||||
if let inputState = try? container.decode(ChatTextInputState.self, forKey: "is") {
|
||||
self.inputState = inputState
|
||||
} else {
|
||||
self.inputState = ChatTextInputState()
|
||||
}
|
||||
|
||||
self.disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup")
|
||||
self.inputTextMaxLength = try? container.decodeIfPresent(Int32.self, forKey: "tl")
|
||||
}
|
||||
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode(self.messageId.peerId.toInt64(), forKey: "mp")
|
||||
try container.encode(self.messageId.namespace, forKey: "mn")
|
||||
try container.encode(self.messageId.id, forKey: "mi")
|
||||
|
||||
try container.encode(self.inputState, forKey: "is")
|
||||
|
||||
try container.encodeIfPresent(self.disableUrlPreview, forKey: "dup")
|
||||
try container.encodeIfPresent(self.inputTextMaxLength, forKey: "tl")
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
/*public init(decoder: PostboxDecoder) {
|
||||
self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp", orElse: 0)), namespace: decoder.decodeInt32ForKey("mn", orElse: 0), id: decoder.decodeInt32ForKey("mi", orElse: 0))
|
||||
if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState {
|
||||
self.inputState = inputState
|
||||
@ -75,7 +128,7 @@ public struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "ml")
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool {
|
||||
return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreview == rhs.disableUrlPreview && lhs.inputTextMaxLength == rhs.inputTextMaxLength
|
||||
@ -322,7 +375,7 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.timestamp = decoder.decodeInt32ForKey("ts", orElse: 0)
|
||||
if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState {
|
||||
if let inputState = decoder.decode(ChatTextInputState.self, forKey: "is") {
|
||||
self.composeInputState = inputState
|
||||
} else {
|
||||
self.composeInputState = ChatTextInputState()
|
||||
@ -345,12 +398,12 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
} else {
|
||||
self.forwardMessageIds = nil
|
||||
}
|
||||
if let editMessage = decoder.decodeObjectForKey("em", decoder: { ChatEditMessageState(decoder: $0) }) as? ChatEditMessageState {
|
||||
if let editMessage = decoder.decode(ChatEditMessageState.self, forKey: "em") {
|
||||
self.editMessage = editMessage
|
||||
} else {
|
||||
self.editMessage = nil
|
||||
}
|
||||
if let selectionState = decoder.decodeObjectForKey("ss", decoder: { return ChatInterfaceSelectionState(decoder: $0) }) as? ChatInterfaceSelectionState {
|
||||
if let selectionState = decoder.decode(ChatInterfaceSelectionState.self, forKey: "ss") {
|
||||
self.selectionState = selectionState
|
||||
} else {
|
||||
self.selectionState = nil
|
||||
@ -372,7 +425,7 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.timestamp, forKey: "ts")
|
||||
encoder.encodeObject(self.composeInputState, forKey: "is")
|
||||
encoder.encode(self.composeInputState, forKey: "is")
|
||||
if let composeDisableUrlPreview = self.composeDisableUrlPreview {
|
||||
encoder.encodeString(composeDisableUrlPreview, forKey: "dup")
|
||||
} else {
|
||||
@ -395,12 +448,12 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
encoder.encodeNil(forKey: "fm")
|
||||
}
|
||||
if let editMessage = self.editMessage {
|
||||
encoder.encodeObject(editMessage, forKey: "em")
|
||||
encoder.encode(editMessage, forKey: "em")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "em")
|
||||
}
|
||||
if let selectionState = self.selectionState {
|
||||
encoder.encodeObject(selectionState, forKey: "ss")
|
||||
encoder.encode(selectionState, forKey: "ss")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "ss")
|
||||
}
|
||||
|
14
submodules/ComponentFlow/BUILD
Normal file
14
submodules/ComponentFlow/BUILD
Normal file
@ -0,0 +1,14 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ComponentFlow",
|
||||
module_name = "ComponentFlow",
|
||||
srcs = glob([
|
||||
"Source/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,78 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension Transition.Appear {
|
||||
static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.Appear {
|
||||
return Transition.Appear { component, view, transition in
|
||||
if scale {
|
||||
transition.animateScale(view: view, from: 0.01, to: 1.0)
|
||||
}
|
||||
if alpha {
|
||||
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func scaleIn() -> Transition.Appear {
|
||||
return Transition.Appear { component, view, transition in
|
||||
transition.animateScale(view: view, from: 0.01, to: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.AppearWithGuide {
|
||||
static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.AppearWithGuide {
|
||||
return Transition.AppearWithGuide { component, view, guide, transition in
|
||||
if scale {
|
||||
transition.animateScale(view: view, from: 0.01, to: 1.0)
|
||||
}
|
||||
if alpha {
|
||||
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
|
||||
}
|
||||
transition.animatePosition(view: view, from: CGPoint(x: guide.x - view.center.x, y: guide.y - view.center.y), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.Disappear {
|
||||
static let `default` = Transition.Disappear { view, transition, completion in
|
||||
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.DisappearWithGuide {
|
||||
static func `default`(alpha: Bool = true) -> Transition.DisappearWithGuide {
|
||||
return Transition.DisappearWithGuide { stage, view, guide, transition, completion in
|
||||
switch stage {
|
||||
case .begin:
|
||||
if alpha {
|
||||
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: guide.x - view.bounds.width / 2.0, y: guide.y - view.bounds.height / 2.0), size: view.bounds.size), completion: { _ in
|
||||
if !alpha {
|
||||
completion()
|
||||
}
|
||||
})
|
||||
case .update:
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: guide.x - view.bounds.width / 2.0, y: guide.y - view.bounds.height / 2.0), size: view.bounds.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.Update {
|
||||
static let `default` = Transition.Update { component, view, transition in
|
||||
let frame = component.size.centered(around: component._position ?? CGPoint())
|
||||
if view.frame != frame {
|
||||
transition.setFrame(view: view, frame: frame)
|
||||
}
|
||||
let opacity = component._opacity ?? 1.0
|
||||
if view.alpha != opacity {
|
||||
transition.setAlpha(view: view, alpha: opacity)
|
||||
}
|
||||
}
|
||||
}
|
798
submodules/ComponentFlow/Source/Base/CombinedComponent.swift
Normal file
798
submodules/ComponentFlow/Source/Base/CombinedComponent.swift
Normal file
@ -0,0 +1,798 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private func updateChildAnyComponent<EnvironmentType>(
|
||||
id: _AnyChildComponent.Id,
|
||||
component: AnyComponent<EnvironmentType>,
|
||||
view: UIView,
|
||||
availableSize: CGSize,
|
||||
transition: Transition
|
||||
) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
|
||||
if !parentContext.updateContext.updatedViews.insert(id).inserted {
|
||||
preconditionFailure("Child component can only be processed once")
|
||||
}
|
||||
|
||||
let context = view.context(component: component)
|
||||
var isEnvironmentUpdated = false
|
||||
var isStateUpdated = false
|
||||
var isComponentUpdated = false
|
||||
var availableSizeUpdated = false
|
||||
|
||||
if context.environment.calculateIsUpdated() {
|
||||
context.environment._isUpdated = false
|
||||
isEnvironmentUpdated = true
|
||||
}
|
||||
|
||||
if context.erasedState.isUpdated {
|
||||
context.erasedState.isUpdated = false
|
||||
isStateUpdated = true
|
||||
}
|
||||
|
||||
if context.erasedComponent != component {
|
||||
isComponentUpdated = true
|
||||
}
|
||||
context.erasedComponent = component
|
||||
|
||||
if context.layoutResult.availableSize != availableSize {
|
||||
context.layoutResult.availableSize = availableSize
|
||||
availableSizeUpdated = true
|
||||
}
|
||||
|
||||
let isUpdated = isEnvironmentUpdated || isStateUpdated || isComponentUpdated || availableSizeUpdated
|
||||
|
||||
if !isUpdated, let size = context.layoutResult.size {
|
||||
return _UpdatedChildComponent(
|
||||
id: id,
|
||||
component: component,
|
||||
view: view,
|
||||
context: context,
|
||||
size: size
|
||||
)
|
||||
} else {
|
||||
let size = component._update(
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
context.layoutResult.size = size
|
||||
|
||||
return _UpdatedChildComponent(
|
||||
id: id,
|
||||
component: component,
|
||||
view: view,
|
||||
context: context,
|
||||
size: size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public class _AnyChildComponent {
|
||||
fileprivate enum Id: Hashable {
|
||||
case direct(Int)
|
||||
case mapped(Int, AnyHashable)
|
||||
}
|
||||
|
||||
fileprivate var directId: Int {
|
||||
return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
}
|
||||
|
||||
public final class _ConcreteChildComponent<ComponentType: Component>: _AnyChildComponent {
|
||||
fileprivate var id: Id {
|
||||
return .direct(self.directId)
|
||||
}
|
||||
|
||||
public func update(component: ComponentType, @EnvironmentBuilder environment: () -> Environment<ComponentType.EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
if !parentContext.updateContext.configuredViews.insert(self.id).inserted {
|
||||
preconditionFailure("Child component can only be configured once")
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
|
||||
let view: ComponentType.View
|
||||
if let current = parentContext.childViews[self.id] {
|
||||
// TODO: Check if the type is the same
|
||||
view = current.view as! ComponentType.View
|
||||
} else {
|
||||
view = component.makeView()
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
let context = view.context(component: component)
|
||||
EnvironmentBuilder._environment = context.erasedEnvironment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
return updateChildAnyComponent(
|
||||
id: self.id,
|
||||
component: AnyComponent(component),
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _ConcreteChildComponent where ComponentType.EnvironmentType == Empty {
|
||||
func update(component: ComponentType, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _UpdatedChildComponentGuide {
|
||||
fileprivate let instance: _ChildComponentGuide
|
||||
|
||||
fileprivate init(instance: _ChildComponentGuide) {
|
||||
self.instance = instance
|
||||
}
|
||||
}
|
||||
|
||||
public final class _ChildComponentGuide {
|
||||
fileprivate var directId: Int {
|
||||
return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
|
||||
fileprivate var id: _AnyChildComponent.Id {
|
||||
return .direct(self.directId)
|
||||
}
|
||||
|
||||
public func update(position: CGPoint, transition: Transition) -> _UpdatedChildComponentGuide {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
|
||||
let previousPosition = parentContext.guides[self.id]
|
||||
|
||||
if parentContext.updateContext.configuredGuides.updateValue(_AnyCombinedComponentContext.UpdateContext.ConfiguredGuide(previousPosition: previousPosition ?? position, position: position), forKey: self.id) != nil {
|
||||
preconditionFailure("Child guide can only be configured once")
|
||||
}
|
||||
|
||||
for disappearingView in parentContext.disappearingChildViews {
|
||||
if disappearingView.guideId == self.id {
|
||||
disappearingView.transitionWithGuide?(
|
||||
stage: .update,
|
||||
view: disappearingView.view,
|
||||
guide: position,
|
||||
transition: transition,
|
||||
completion: disappearingView.completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return _UpdatedChildComponentGuide(instance: self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _UpdatedChildComponent {
|
||||
fileprivate let id: _AnyChildComponent.Id
|
||||
fileprivate let component: _TypeErasedComponent
|
||||
fileprivate let view: UIView
|
||||
fileprivate let context: _TypeErasedComponentContext
|
||||
|
||||
public let size: CGSize
|
||||
|
||||
var _removed: Bool = false
|
||||
var _position: CGPoint?
|
||||
var _opacity: CGFloat?
|
||||
|
||||
fileprivate var transitionAppear: Transition.Appear?
|
||||
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
|
||||
fileprivate var transitionDisappear: Transition.Disappear?
|
||||
fileprivate var transitionDisappearWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)?
|
||||
fileprivate var transitionUpdate: Transition.Update?
|
||||
fileprivate var gestures: [Gesture] = []
|
||||
|
||||
fileprivate init(
|
||||
id: _AnyChildComponent.Id,
|
||||
component: _TypeErasedComponent,
|
||||
view: UIView,
|
||||
context: _TypeErasedComponentContext,
|
||||
size: CGSize
|
||||
) {
|
||||
self.id = id
|
||||
self.component = component
|
||||
self.view = view
|
||||
self.context = context
|
||||
self.size = size
|
||||
}
|
||||
|
||||
@discardableResult public func appear(_ transition: Transition.Appear) -> _UpdatedChildComponent {
|
||||
self.transitionAppear = transition
|
||||
self.transitionAppearWithGuide = nil
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func appear(_ transition: Transition.AppearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent {
|
||||
self.transitionAppear = nil
|
||||
self.transitionAppearWithGuide = (transition, guide.instance.id)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func disappear(_ transition: Transition.Disappear) -> _UpdatedChildComponent {
|
||||
self.transitionDisappear = transition
|
||||
self.transitionDisappearWithGuide = nil
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func disappear(_ transition: Transition.DisappearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent {
|
||||
self.transitionDisappear = nil
|
||||
self.transitionDisappearWithGuide = (transition, guide.instance.id)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func update(_ transition: Transition.Update) -> _UpdatedChildComponent {
|
||||
self.transitionUpdate = transition
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func removed(_ removed: Bool) -> _UpdatedChildComponent {
|
||||
self._removed = removed
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func position(_ position: CGPoint) -> _UpdatedChildComponent {
|
||||
self._position = position
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func opacity(_ opacity: CGFloat) -> _UpdatedChildComponent {
|
||||
self._opacity = opacity
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent {
|
||||
self.gestures.append(gesture)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public final class _EnvironmentChildComponent<EnvironmentType>: _AnyChildComponent {
|
||||
fileprivate var id: Id {
|
||||
return .direct(self.directId)
|
||||
}
|
||||
|
||||
func update(component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
if !parentContext.updateContext.configuredViews.insert(self.id).inserted {
|
||||
preconditionFailure("Child component can only be configured once")
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
|
||||
let view: UIView
|
||||
if let current = parentContext.childViews[self.id] {
|
||||
// Check if the type is the same
|
||||
view = current.view
|
||||
} else {
|
||||
view = component._makeView()
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
return updateChildAnyComponent(
|
||||
id: self.id,
|
||||
component: component,
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _EnvironmentChildComponent where EnvironmentType == Empty {
|
||||
func update(component: AnyComponent<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _EnvironmentChildComponent {
|
||||
func update<ComponentType: Component>(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType {
|
||||
return self.update(component: AnyComponent(component), environment: environment, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
|
||||
func update<ComponentType: Component>(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType, EnvironmentType == Empty {
|
||||
return self.update(component: AnyComponent(component), environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _EnvironmentChildComponentFromMap<EnvironmentType>: _AnyChildComponent {
|
||||
private let id: Id
|
||||
|
||||
fileprivate init(id: Id) {
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public func update(component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
if !parentContext.updateContext.configuredViews.insert(self.id).inserted {
|
||||
preconditionFailure("Child component can only be configured once")
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
|
||||
let view: UIView
|
||||
if let current = parentContext.childViews[self.id] {
|
||||
// Check if the type is the same
|
||||
view = current.view
|
||||
} else {
|
||||
view = component._makeView()
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
return updateChildAnyComponent(
|
||||
id: self.id,
|
||||
component: component,
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _EnvironmentChildComponentFromMap where EnvironmentType == Empty {
|
||||
func update(component: AnyComponent<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _EnvironmentChildComponentMap<EnvironmentType, Key: Hashable> {
|
||||
private var directId: Int {
|
||||
return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
|
||||
public subscript(_ key: Key) -> _EnvironmentChildComponentFromMap<EnvironmentType> {
|
||||
get {
|
||||
return _EnvironmentChildComponentFromMap<EnvironmentType>(id: .mapped(self.directId, key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class CombinedComponentContext<ComponentType: Component> {
|
||||
fileprivate let escapeGuard = EscapeGuard()
|
||||
|
||||
private let context: ComponentContext<ComponentType>
|
||||
public let view: UIView
|
||||
|
||||
public let component: ComponentType
|
||||
public let availableSize: CGSize
|
||||
public let transition: Transition
|
||||
private let addImpl: (_ updatedComponent: _UpdatedChildComponent) -> Void
|
||||
|
||||
public var environment: Environment<ComponentType.EnvironmentType> {
|
||||
return self.context.environment
|
||||
}
|
||||
public var state: ComponentType.State {
|
||||
return self.context.state
|
||||
}
|
||||
|
||||
fileprivate init(
|
||||
context: ComponentContext<ComponentType>,
|
||||
view: UIView,
|
||||
component: ComponentType,
|
||||
availableSize: CGSize,
|
||||
transition: Transition,
|
||||
add: @escaping (_ updatedComponent: _UpdatedChildComponent) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.view = view
|
||||
self.component = component
|
||||
self.availableSize = availableSize
|
||||
self.transition = transition
|
||||
self.addImpl = add
|
||||
}
|
||||
|
||||
public func add(_ updatedComponent: _UpdatedChildComponent) {
|
||||
self.addImpl(updatedComponent)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CombinedComponent: Component {
|
||||
typealias Body = (CombinedComponentContext<Self>) -> CGSize
|
||||
|
||||
static var body: Body { get }
|
||||
}
|
||||
|
||||
private class _AnyCombinedComponentContext {
|
||||
class UpdateContext {
|
||||
struct ConfiguredGuide {
|
||||
var previousPosition: CGPoint
|
||||
var position: CGPoint
|
||||
}
|
||||
|
||||
var configuredViews: Set<_AnyChildComponent.Id> = Set()
|
||||
var updatedViews: Set<_AnyChildComponent.Id> = Set()
|
||||
var configuredGuides: [_AnyChildComponent.Id: ConfiguredGuide] = [:]
|
||||
}
|
||||
|
||||
private static var _current: _AnyCombinedComponentContext?
|
||||
static var current: _AnyCombinedComponentContext {
|
||||
return self._current!
|
||||
}
|
||||
|
||||
static func push(_ context: _AnyCombinedComponentContext) -> _AnyCombinedComponentContext? {
|
||||
let previous = self._current
|
||||
|
||||
precondition(context._updateContext == nil)
|
||||
context._updateContext = UpdateContext()
|
||||
self._current = context
|
||||
|
||||
return previous
|
||||
}
|
||||
|
||||
static func pop(_ context: _AnyCombinedComponentContext, stack: _AnyCombinedComponentContext?) {
|
||||
precondition(context._updateContext != nil)
|
||||
context._updateContext = nil
|
||||
|
||||
self._current = stack
|
||||
}
|
||||
|
||||
class ChildView {
|
||||
let view: UIView
|
||||
var index: Int
|
||||
var transition: Transition.Disappear?
|
||||
var transitionWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)?
|
||||
|
||||
var gestures: [UInt: UIGestureRecognizer] = [:]
|
||||
|
||||
init(view: UIView, index: Int) {
|
||||
self.view = view
|
||||
self.index = index
|
||||
}
|
||||
|
||||
func updateGestures(_ gestures: [Gesture]) {
|
||||
var validIds: [UInt] = []
|
||||
for gesture in gestures {
|
||||
validIds.append(gesture.id.id)
|
||||
if let current = self.gestures[gesture.id.id] {
|
||||
gesture.update(gesture: current)
|
||||
} else {
|
||||
let gestureInstance = gesture.create()
|
||||
self.gestures[gesture.id.id] = gestureInstance
|
||||
self.view.addGestureRecognizer(gestureInstance)
|
||||
}
|
||||
}
|
||||
var removeIds: [UInt] = []
|
||||
for id in self.gestures.keys {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
if let gestureInstance = self.gestures.removeValue(forKey: id) {
|
||||
self.view.removeGestureRecognizer(gestureInstance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DisappearingChildView {
|
||||
let view: UIView
|
||||
let guideId: _AnyChildComponent.Id?
|
||||
let transition: Transition.Disappear?
|
||||
let transitionWithGuide: Transition.DisappearWithGuide?
|
||||
let completion: () -> Void
|
||||
|
||||
init(
|
||||
view: UIView,
|
||||
guideId: _AnyChildComponent.Id?,
|
||||
transition: Transition.Disappear?,
|
||||
transitionWithGuide: Transition.DisappearWithGuide?,
|
||||
completion: @escaping () -> Void
|
||||
) {
|
||||
self.view = view
|
||||
self.guideId = guideId
|
||||
self.transition = transition
|
||||
self.transitionWithGuide = transitionWithGuide
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
var childViews: [_AnyChildComponent.Id: ChildView] = [:]
|
||||
var childViewIndices: [_AnyChildComponent.Id] = []
|
||||
var guides: [_AnyChildComponent.Id: CGPoint] = [:]
|
||||
var disappearingChildViews: [DisappearingChildView] = []
|
||||
|
||||
private var _updateContext: UpdateContext?
|
||||
var updateContext: UpdateContext {
|
||||
return self._updateContext!
|
||||
}
|
||||
}
|
||||
|
||||
private final class _CombinedComponentContext<ComponentType: CombinedComponent>: _AnyCombinedComponentContext {
|
||||
var body: ComponentType.Body?
|
||||
}
|
||||
|
||||
private var UIView_CombinedComponentContextKey: Int?
|
||||
|
||||
private extension UIView {
|
||||
func getCombinedComponentContext<ComponentType: CombinedComponent>(_ type: ComponentType.Type) -> _CombinedComponentContext<ComponentType> {
|
||||
if let context = objc_getAssociatedObject(self, &UIView_CombinedComponentContextKey) as? _CombinedComponentContext<ComponentType> {
|
||||
return context
|
||||
} else {
|
||||
let context = _CombinedComponentContext<ComponentType>()
|
||||
objc_setAssociatedObject(self, &UIView_CombinedComponentContextKey, context, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
return context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition {
|
||||
final class Appear {
|
||||
private let f: (_UpdatedChildComponent, UIView, Transition) -> Void
|
||||
|
||||
public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) {
|
||||
self.f(component, view, transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class AppearWithGuide {
|
||||
private let f: (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void
|
||||
|
||||
public init(_ f: @escaping (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, guide: CGPoint, transition: Transition) {
|
||||
self.f(component, view, guide, transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class Disappear {
|
||||
private let f: (UIView, Transition, @escaping () -> Void) -> Void
|
||||
|
||||
public init(_ f: @escaping (UIView, Transition, @escaping () -> Void) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(view: UIView, transition: Transition, completion: @escaping () -> Void) {
|
||||
self.f(view, transition, completion)
|
||||
}
|
||||
}
|
||||
|
||||
final class DisappearWithGuide {
|
||||
public enum Stage {
|
||||
case begin
|
||||
case update
|
||||
}
|
||||
|
||||
private let f: (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void
|
||||
|
||||
public init(_ f: @escaping (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void
|
||||
) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(stage: Stage, view: UIView, guide: CGPoint, transition: Transition, completion: @escaping () -> Void) {
|
||||
self.f(stage, view, guide, transition, completion)
|
||||
}
|
||||
}
|
||||
|
||||
final class Update {
|
||||
private let f: (_UpdatedChildComponent, UIView, Transition) -> Void
|
||||
|
||||
public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) {
|
||||
self.f(component, view, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension CombinedComponent {
|
||||
func makeView() -> UIView {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let context = view.getCombinedComponentContext(Self.self)
|
||||
|
||||
let storedBody: Body
|
||||
if let current = context.body {
|
||||
storedBody = current
|
||||
} else {
|
||||
storedBody = Self.body
|
||||
context.body = storedBody
|
||||
}
|
||||
|
||||
let viewContext = view.context(component: self)
|
||||
|
||||
var nextChildIndex = 0
|
||||
var addedChildIds = Set<_AnyChildComponent.Id>()
|
||||
|
||||
let contextStack = _AnyCombinedComponentContext.push(context)
|
||||
|
||||
let escapeStatus: EscapeGuard.Status
|
||||
let size: CGSize
|
||||
do {
|
||||
let bodyContext = CombinedComponentContext<Self>(
|
||||
context: viewContext,
|
||||
view: view,
|
||||
component: self,
|
||||
availableSize: availableSize,
|
||||
transition: transition,
|
||||
add: { updatedChild in
|
||||
if !addedChildIds.insert(updatedChild.id).inserted {
|
||||
preconditionFailure("Child component can only be added once")
|
||||
}
|
||||
|
||||
let index = nextChildIndex
|
||||
nextChildIndex += 1
|
||||
|
||||
if let previousView = context.childViews[updatedChild.id] {
|
||||
precondition(updatedChild.view === previousView.view)
|
||||
|
||||
if index != previousView.index {
|
||||
assert(index < previousView.index)
|
||||
for i in index ..< previousView.index {
|
||||
if let moveView = context.childViews[context.childViewIndices[i]] {
|
||||
moveView.index += 1
|
||||
}
|
||||
}
|
||||
context.childViewIndices.remove(at: previousView.index)
|
||||
context.childViewIndices.insert(updatedChild.id, at: index)
|
||||
previousView.index = index
|
||||
view.insertSubview(previousView.view, at: index)
|
||||
}
|
||||
|
||||
previousView.updateGestures(updatedChild.gestures)
|
||||
previousView.transition = updatedChild.transitionDisappear
|
||||
previousView.transitionWithGuide = updatedChild.transitionDisappearWithGuide
|
||||
|
||||
(updatedChild.transitionUpdate ?? Transition.Update.default)(component: updatedChild, view: updatedChild.view, transition: transition)
|
||||
} else {
|
||||
for i in index ..< context.childViewIndices.count {
|
||||
if let moveView = context.childViews[context.childViewIndices[i]] {
|
||||
moveView.index += 1
|
||||
}
|
||||
}
|
||||
|
||||
context.childViewIndices.insert(updatedChild.id, at: index)
|
||||
let childView = _AnyCombinedComponentContext.ChildView(view: updatedChild.view, index: index)
|
||||
context.childViews[updatedChild.id] = childView
|
||||
|
||||
childView.updateGestures(updatedChild.gestures)
|
||||
childView.transition = updatedChild.transitionDisappear
|
||||
childView.transitionWithGuide = updatedChild.transitionDisappearWithGuide
|
||||
|
||||
view.insertSubview(updatedChild.view, at: index)
|
||||
|
||||
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
|
||||
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
||||
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
|
||||
guard let viewContext = viewContext else {
|
||||
return
|
||||
}
|
||||
viewContext.state.updated(transition: transition)
|
||||
}
|
||||
|
||||
if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide {
|
||||
guard let guide = context.updateContext.configuredGuides[transitionAppearWithGuide.1] else {
|
||||
preconditionFailure("Guide should be configured before using")
|
||||
}
|
||||
transitionAppearWithGuide.0(
|
||||
component: updatedChild,
|
||||
view: updatedChild.view,
|
||||
guide: guide.previousPosition,
|
||||
transition: transition
|
||||
)
|
||||
} else if let transitionAppear = updatedChild.transitionAppear {
|
||||
transitionAppear(
|
||||
component: updatedChild,
|
||||
view: updatedChild.view,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
escapeStatus = bodyContext.escapeGuard.status
|
||||
size = storedBody(bodyContext)
|
||||
}
|
||||
|
||||
assert(escapeStatus.isDeallocated, "Body context should not be stored for later use")
|
||||
|
||||
if nextChildIndex < context.childViewIndices.count {
|
||||
for i in nextChildIndex ..< context.childViewIndices.count {
|
||||
let id = context.childViewIndices[i]
|
||||
if let childView = context.childViews.removeValue(forKey: id) {
|
||||
let view = childView.view
|
||||
let completion: () -> Void = { [weak context, weak view] in
|
||||
view?.removeFromSuperview()
|
||||
|
||||
if let context = context {
|
||||
for i in 0 ..< context.disappearingChildViews.count {
|
||||
if context.disappearingChildViews[i].view === view {
|
||||
context.disappearingChildViews.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let transitionWithGuide = childView.transitionWithGuide {
|
||||
guard let guide = context.updateContext.configuredGuides[transitionWithGuide.1] else {
|
||||
preconditionFailure("Guide should be configured before using")
|
||||
}
|
||||
context.disappearingChildViews.append(_AnyCombinedComponentContext.DisappearingChildView(
|
||||
view: view,
|
||||
guideId: transitionWithGuide.1,
|
||||
transition: nil,
|
||||
transitionWithGuide: transitionWithGuide.0,
|
||||
completion: completion
|
||||
))
|
||||
view.isUserInteractionEnabled = false
|
||||
transitionWithGuide.0(
|
||||
stage: .begin,
|
||||
view: view,
|
||||
guide: guide.position,
|
||||
transition: transition,
|
||||
completion: completion
|
||||
)
|
||||
} else if let simpleTransition = childView.transition {
|
||||
context.disappearingChildViews.append(_AnyCombinedComponentContext.DisappearingChildView(
|
||||
view: view,
|
||||
guideId: nil,
|
||||
transition: simpleTransition,
|
||||
transitionWithGuide: nil,
|
||||
completion: completion
|
||||
))
|
||||
view.isUserInteractionEnabled = false
|
||||
simpleTransition(
|
||||
view: view,
|
||||
transition: transition,
|
||||
completion: completion
|
||||
)
|
||||
} else {
|
||||
childView.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
context.childViewIndices.removeSubrange(nextChildIndex...)
|
||||
}
|
||||
|
||||
if addedChildIds != context.updateContext.updatedViews {
|
||||
preconditionFailure("Updated and added child lists do not match")
|
||||
}
|
||||
|
||||
context.guides.removeAll()
|
||||
for (id, guide) in context.updateContext.configuredGuides {
|
||||
context.guides[id] = guide.position
|
||||
}
|
||||
|
||||
_AnyCombinedComponentContext.pop(context, stack: contextStack)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public extension CombinedComponent {
|
||||
static func Child<Environment>(environment: Environment.Type) -> _EnvironmentChildComponent<Environment> {
|
||||
return _EnvironmentChildComponent<Environment>()
|
||||
}
|
||||
|
||||
static func ChildMap<Environment, Key: Hashable>(environment: Environment.Type, keyedBy keyType: Key.Type) -> _EnvironmentChildComponentMap<Environment, Key> {
|
||||
return _EnvironmentChildComponentMap<Environment, Key>()
|
||||
}
|
||||
|
||||
static func Child<ComponentType: Component>(_ type: ComponentType.Type) -> _ConcreteChildComponent<ComponentType> {
|
||||
return _ConcreteChildComponent<ComponentType>()
|
||||
}
|
||||
|
||||
static func Guide() -> _ChildComponentGuide {
|
||||
return _ChildComponentGuide()
|
||||
}
|
||||
}
|
203
submodules/ComponentFlow/Source/Base/Component.swift
Normal file
203
submodules/ComponentFlow/Source/Base/Component.swift
Normal file
@ -0,0 +1,203 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ObjectiveC
|
||||
|
||||
public class ComponentLayoutResult {
|
||||
var availableSize: CGSize?
|
||||
var size: CGSize?
|
||||
}
|
||||
|
||||
public protocol _TypeErasedComponentContext: AnyObject {
|
||||
var erasedEnvironment: _Environment { get }
|
||||
var erasedState: ComponentState { get }
|
||||
|
||||
var layoutResult: ComponentLayoutResult { get }
|
||||
}
|
||||
|
||||
class AnyComponentContext<EnvironmentType>: _TypeErasedComponentContext {
|
||||
var erasedComponent: AnyComponent<EnvironmentType> {
|
||||
get {
|
||||
preconditionFailure()
|
||||
} set(value) {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
var erasedState: ComponentState {
|
||||
preconditionFailure()
|
||||
}
|
||||
var erasedEnvironment: _Environment {
|
||||
return self.environment
|
||||
}
|
||||
|
||||
let layoutResult: ComponentLayoutResult
|
||||
var environment: Environment<EnvironmentType>
|
||||
|
||||
init(environment: Environment<EnvironmentType>) {
|
||||
self.layoutResult = ComponentLayoutResult()
|
||||
self.environment = environment
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentContext<ComponentType: Component>: AnyComponentContext<ComponentType.EnvironmentType> {
|
||||
override var erasedComponent: AnyComponent<ComponentType.EnvironmentType> {
|
||||
get {
|
||||
return AnyComponent(self.component)
|
||||
} set(value) {
|
||||
self.component = value.wrapped as! ComponentType
|
||||
}
|
||||
}
|
||||
|
||||
var component: ComponentType
|
||||
let state: ComponentType.State
|
||||
|
||||
override var erasedState: ComponentState {
|
||||
return self.state
|
||||
}
|
||||
|
||||
init(component: ComponentType, environment: Environment<ComponentType.EnvironmentType>, state: ComponentType.State) {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
super.init(environment: environment)
|
||||
}
|
||||
}
|
||||
|
||||
private var UIView_TypeErasedComponentContextKey: Int?
|
||||
|
||||
extension UIView {
|
||||
func context<EnvironmentType>(component: AnyComponent<EnvironmentType>) -> AnyComponentContext<EnvironmentType> {
|
||||
return self.context(typeErasedComponent: component) as! AnyComponentContext<EnvironmentType>
|
||||
}
|
||||
|
||||
func context<ComponentType: Component>(component: ComponentType) -> ComponentContext<ComponentType> {
|
||||
return self.context(typeErasedComponent: component) as! ComponentContext<ComponentType>
|
||||
}
|
||||
|
||||
func context(typeErasedComponent component: _TypeErasedComponent) -> _TypeErasedComponentContext{
|
||||
if let context = objc_getAssociatedObject(self, &UIView_TypeErasedComponentContextKey) as? _TypeErasedComponentContext {
|
||||
return context
|
||||
} else {
|
||||
let context = component._makeContext()
|
||||
objc_setAssociatedObject(self, &UIView_TypeErasedComponentContextKey, context, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
return context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ComponentState {
|
||||
var _updated: ((Transition) -> Void)?
|
||||
var isUpdated: Bool = false
|
||||
|
||||
public final func updated(transition: Transition = .immediate) {
|
||||
self.isUpdated = true
|
||||
self._updated?(transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class EmptyComponentState: ComponentState {
|
||||
}
|
||||
|
||||
public protocol _TypeErasedComponent {
|
||||
func _makeView() -> UIView
|
||||
func _makeContext() -> _TypeErasedComponentContext
|
||||
func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize
|
||||
func _isEqual(to other: _TypeErasedComponent) -> Bool
|
||||
}
|
||||
|
||||
public protocol Component: _TypeErasedComponent, Equatable {
|
||||
associatedtype EnvironmentType = Empty
|
||||
associatedtype View: UIView = UIView
|
||||
associatedtype State: ComponentState = EmptyComponentState
|
||||
|
||||
func makeView() -> View
|
||||
func makeState() -> State
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize
|
||||
}
|
||||
|
||||
public extension Component {
|
||||
func _makeView() -> UIView {
|
||||
return self.makeView()
|
||||
}
|
||||
|
||||
func _makeContext() -> _TypeErasedComponentContext {
|
||||
return ComponentContext<Self>(component: self, environment: Environment<EnvironmentType>(), state: self.makeState())
|
||||
}
|
||||
|
||||
func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return self.update(view: view as! Self.View, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
|
||||
func _isEqual(to other: _TypeErasedComponent) -> Bool {
|
||||
if let other = other as? Self {
|
||||
return self == other
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Component where Self.View == UIView {
|
||||
func makeView() -> UIView {
|
||||
return UIView()
|
||||
}
|
||||
}
|
||||
|
||||
public extension Component where Self.State == EmptyComponentState {
|
||||
func makeState() -> State {
|
||||
return EmptyComponentState()
|
||||
}
|
||||
}
|
||||
|
||||
public class ComponentGesture {
|
||||
public static func tap(action: @escaping() -> Void) -> ComponentGesture {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
public class AnyComponent<EnvironmentType>: _TypeErasedComponent, Equatable {
|
||||
fileprivate let wrapped: _TypeErasedComponent
|
||||
|
||||
public init<ComponentType: Component>(_ component: ComponentType) where ComponentType.EnvironmentType == EnvironmentType {
|
||||
self.wrapped = component
|
||||
}
|
||||
|
||||
public static func ==(lhs: AnyComponent<EnvironmentType>, rhs: AnyComponent<EnvironmentType>) -> Bool {
|
||||
return lhs.wrapped._isEqual(to: rhs.wrapped)
|
||||
}
|
||||
|
||||
public func _makeView() -> UIView {
|
||||
return self.wrapped._makeView()
|
||||
}
|
||||
|
||||
public func _makeContext() -> _TypeErasedComponentContext {
|
||||
return self.wrapped._makeContext()
|
||||
}
|
||||
|
||||
public func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return self.wrapped._update(view: view, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
|
||||
public func _isEqual(to other: _TypeErasedComponent) -> Bool {
|
||||
return self.wrapped._isEqual(to: other)
|
||||
}
|
||||
}
|
||||
|
||||
public final class AnyComponentWithIdentity<Environment>: Equatable {
|
||||
public let id: AnyHashable
|
||||
public let component: AnyComponent<Environment>
|
||||
|
||||
public init<IdType: Hashable>(id: IdType, component: AnyComponent<Environment>) {
|
||||
self.id = AnyHashable(id)
|
||||
self.component = component
|
||||
}
|
||||
|
||||
public static func == (lhs: AnyComponentWithIdentity<Environment>, rhs: AnyComponentWithIdentity<Environment>) -> Bool {
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.component != rhs.component {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
219
submodules/ComponentFlow/Source/Base/Environment.swift
Normal file
219
submodules/ComponentFlow/Source/Base/Environment.swift
Normal file
@ -0,0 +1,219 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class Empty: Equatable {
|
||||
static let shared: Empty = Empty()
|
||||
|
||||
public static func ==(lhs: Empty, rhs: Empty) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public class _Environment {
|
||||
fileprivate var data: [Int: _EnvironmentValue] = [:]
|
||||
var _isUpdated: Bool = false
|
||||
|
||||
func calculateIsUpdated() -> Bool {
|
||||
if self._isUpdated {
|
||||
return true
|
||||
}
|
||||
for (_, item) in self.data {
|
||||
if let parentEnvironment = item.parentEnvironment, parentEnvironment.calculateIsUpdated() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate func set<T: Equatable>(index: Int, value: EnvironmentValue<T>) {
|
||||
if let current = self.data[index] {
|
||||
self.data[index] = value
|
||||
if current as! EnvironmentValue<T> != value {
|
||||
self._isUpdated = true
|
||||
}
|
||||
} else {
|
||||
self.data[index] = value
|
||||
self._isUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum EnvironmentValueStorage<T> {
|
||||
case direct(T)
|
||||
case reference(_Environment, Int)
|
||||
}
|
||||
|
||||
public class _EnvironmentValue {
|
||||
fileprivate let parentEnvironment: _Environment?
|
||||
|
||||
fileprivate init(parentEnvironment: _Environment?) {
|
||||
self.parentEnvironment = parentEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
@dynamicMemberLookup
|
||||
public final class EnvironmentValue<T: Equatable>: _EnvironmentValue, Equatable {
|
||||
private var storage: EnvironmentValueStorage<T>
|
||||
|
||||
fileprivate var value: T {
|
||||
switch self.storage {
|
||||
case let .direct(value):
|
||||
return value
|
||||
case let .reference(environment, index):
|
||||
return (environment.data[index] as! EnvironmentValue<T>).value
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate init(_ value: T) {
|
||||
self.storage = .direct(value)
|
||||
|
||||
super.init(parentEnvironment: nil)
|
||||
}
|
||||
|
||||
fileprivate init(environment: _Environment, index: Int) {
|
||||
self.storage = .reference(environment, index)
|
||||
|
||||
super.init(parentEnvironment: environment)
|
||||
}
|
||||
|
||||
public static func ==(lhs: EnvironmentValue<T>, rhs: EnvironmentValue<T>) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
// TODO: follow the reference chain for faster equality checking
|
||||
return lhs.value == rhs.value
|
||||
}
|
||||
|
||||
public subscript<V>(dynamicMember keyPath: KeyPath<T, V>) -> V {
|
||||
return self.value[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
public class Environment<T>: _Environment {
|
||||
private let file: StaticString
|
||||
private let line: Int
|
||||
|
||||
public init(_ file: StaticString = #file, _ line: Int = #line) {
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
}
|
||||
|
||||
public extension Environment where T == Empty {
|
||||
static let value: Environment<Empty> = {
|
||||
let result = Environment<Empty>()
|
||||
result.set(index: 0, value: EnvironmentValue(Empty()))
|
||||
return result
|
||||
}()
|
||||
}
|
||||
|
||||
public extension Environment {
|
||||
subscript(_ t1: T.Type) -> EnvironmentValue<T> where T: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2>(_ t1: T1.Type) -> EnvironmentValue<T1> where T == (T1, T2), T1: Equatable, T2: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2>(_ t2: T2.Type) -> EnvironmentValue<T2> where T == (T1, T2), T1: Equatable, T2: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 1)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3>(_ t1: T1.Type) -> EnvironmentValue<T1> where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3>(_ t2: T2.Type) -> EnvironmentValue<T2> where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 1)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3>(_ t3: T3.Type) -> EnvironmentValue<T3> where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 2)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t1: T1.Type) -> EnvironmentValue<T1> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t2: T2.Type) -> EnvironmentValue<T2> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 1)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t3: T3.Type) -> EnvironmentValue<T3> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 2)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t4: T4.Type) -> EnvironmentValue<T4> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 3)
|
||||
}
|
||||
}
|
||||
|
||||
@resultBuilder
|
||||
public struct EnvironmentBuilder {
|
||||
static var _environment: _Environment?
|
||||
private static func current<T>(_ type: T.Type) -> Environment<T> {
|
||||
return self._environment as! Environment<T>
|
||||
}
|
||||
|
||||
public struct Partial<T: Equatable> {
|
||||
fileprivate var value: EnvironmentValue<T>
|
||||
}
|
||||
|
||||
public static func buildBlock() -> Environment<Empty> {
|
||||
let result = self.current(Empty.self)
|
||||
result.set(index: 0, value: EnvironmentValue(Empty.shared))
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildExpression<T: Equatable>(_ expression: T) -> Partial<T> {
|
||||
return Partial<T>(value: EnvironmentValue(expression))
|
||||
}
|
||||
|
||||
public static func buildExpression<T: Equatable>(_ expression: EnvironmentValue<T>) -> Partial<T> {
|
||||
return Partial<T>(value: expression)
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable>(_ t1: Partial<T1>) -> Environment<T1> {
|
||||
let result = self.current(T1.self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable, T2: Equatable>(_ t1: Partial<T1>, _ t2: Partial<T2>) -> Environment<(T1, T2)> {
|
||||
let result = self.current((T1, T2).self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
result.set(index: 1, value: t2.value)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable, T2: Equatable, T3: Equatable>(_ t1: Partial<T1>, _ t2: Partial<T2>, _ t3: Partial<T3>) -> Environment<(T1, T2, T3)> {
|
||||
let result = self.current((T1, T2, T3).self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
result.set(index: 1, value: t2.value)
|
||||
result.set(index: 2, value: t3.value)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable>(_ t1: Partial<T1>, _ t2: Partial<T2>, _ t3: Partial<T3>, _ t4: Partial<T4>) -> Environment<(T1, T2, T3, T4)> {
|
||||
let result = self.current((T1, T2, T3, T4).self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
result.set(index: 1, value: t2.value)
|
||||
result.set(index: 2, value: t3.value)
|
||||
result.set(index: 3, value: t4.value)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct ZeroEquatable<T>: Equatable {
|
||||
public var wrappedValue: T
|
||||
|
||||
public init(_ wrappedValue: T) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public static func ==(lhs: ZeroEquatable<T>, rhs: ZeroEquatable<T>) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
270
submodules/ComponentFlow/Source/Base/Transition.swift
Normal file
270
submodules/ComponentFlow/Source/Base/Transition.swift
Normal file
@ -0,0 +1,270 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private extension UIView {
|
||||
static var animationDurationFactor: Double {
|
||||
return 1.0
|
||||
}
|
||||
}
|
||||
|
||||
@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate {
|
||||
private let keyPath: String?
|
||||
var completion: ((Bool) -> Void)?
|
||||
|
||||
init(animation: CAAnimation, completion: ((Bool) -> Void)?) {
|
||||
if let animation = animation as? CABasicAnimation {
|
||||
self.keyPath = animation.keyPath
|
||||
} else {
|
||||
self.keyPath = nil
|
||||
}
|
||||
self.completion = completion
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
|
||||
if let anim = anim as? CABasicAnimation {
|
||||
if anim.keyPath != self.keyPath {
|
||||
return
|
||||
}
|
||||
}
|
||||
if let completion = self.completion {
|
||||
completion(flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeSpringAnimation(keyPath: String) -> CASpringAnimation {
|
||||
let springAnimation = CASpringAnimation(keyPath: keyPath)
|
||||
springAnimation.mass = 3.0;
|
||||
springAnimation.stiffness = 1000.0
|
||||
springAnimation.damping = 500.0
|
||||
springAnimation.duration = 0.5
|
||||
springAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
return springAnimation
|
||||
}
|
||||
|
||||
private extension CALayer {
|
||||
func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
|
||||
switch curve {
|
||||
case .spring:
|
||||
let animation = makeSpringAnimation(keyPath: keyPath)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = .forwards
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
let k = Float(UIView.animationDurationFactor)
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
animation.speed = speed * Float(animation.duration / duration)
|
||||
animation.isAdditive = additive
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor
|
||||
animation.fillMode = .both
|
||||
}
|
||||
|
||||
return animation
|
||||
default:
|
||||
let k = Float(UIView.animationDurationFactor)
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
let animation = CABasicAnimation(keyPath: keyPath)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.duration = duration
|
||||
animation.timingFunction = curve.asTimingFunction()
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = .forwards
|
||||
animation.speed = speed
|
||||
animation.isAdditive = additive
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor
|
||||
animation.fillMode = .both
|
||||
}
|
||||
|
||||
return animation
|
||||
}
|
||||
}
|
||||
|
||||
func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) {
|
||||
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, duration: duration, delay: delay, curve: curve, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
self.add(animation, forKey: additive ? nil : keyPath)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Transition.Animation.Curve {
|
||||
func asTimingFunction() -> CAMediaTimingFunction {
|
||||
switch self {
|
||||
case .easeInOut:
|
||||
return CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
case .spring:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Transition {
|
||||
public enum Animation {
|
||||
public enum Curve {
|
||||
case easeInOut
|
||||
case spring
|
||||
}
|
||||
|
||||
case none
|
||||
case curve(duration: Double, curve: Curve)
|
||||
}
|
||||
|
||||
public var animation: Animation
|
||||
private var _userData: [Any] = []
|
||||
|
||||
public func userData<T>(_ type: T.Type) -> T? {
|
||||
for item in self._userData {
|
||||
if let item = item as? T {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func withUserData(_ userData: Any) -> Transition {
|
||||
var result = self
|
||||
result._userData.append(userData)
|
||||
return result
|
||||
}
|
||||
|
||||
public static var immediate: Transition = Transition(animation: .none)
|
||||
|
||||
public static func easeInOut(duration: Double) -> Transition {
|
||||
return Transition(animation: .curve(duration: duration, curve: .easeInOut))
|
||||
}
|
||||
|
||||
public init(animation: Animation) {
|
||||
self.animation = animation
|
||||
}
|
||||
|
||||
public func setFrame(view: UIView, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||
if view.frame == frame {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
switch self.animation {
|
||||
case .none:
|
||||
view.frame = frame
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousPosition = view.center
|
||||
let previousBounds = view.bounds
|
||||
view.frame = frame
|
||||
|
||||
self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion)
|
||||
self.animateBounds(view: view, from: previousBounds, to: view.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
if view.alpha == alpha {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
switch self.animation {
|
||||
case .none:
|
||||
view.alpha = alpha
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousAlpha = view.alpha
|
||||
view.alpha = alpha
|
||||
self.animateAlpha(view: view, from: previousAlpha, to: alpha, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: fromValue as NSNumber,
|
||||
to: toValue as NSNumber,
|
||||
keyPath: "transform.scale",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: fromValue as NSNumber,
|
||||
to: toValue as NSNumber,
|
||||
keyPath: "opacity",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: NSValue(cgPoint: fromValue),
|
||||
to: NSValue(cgPoint: toValue),
|
||||
keyPath: "position",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateBounds(view: UIView, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
break
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: NSValue(cgRect: fromValue),
|
||||
to: NSValue(cgRect: toValue),
|
||||
keyPath: "bounds",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
68
submodules/ComponentFlow/Source/Components/Button.swift
Normal file
68
submodules/ComponentFlow/Source/Components/Button.swift
Normal file
@ -0,0 +1,68 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class Button: CombinedComponent, Equatable {
|
||||
let content: AnyComponent<Empty>
|
||||
let insets: UIEdgeInsets
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
content: AnyComponent<Empty>,
|
||||
insets: UIEdgeInsets,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.content = content
|
||||
self.insets = insets
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: Button, rhs: Button) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var isHighlighted = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let content = Child(environment: Empty.self)
|
||||
|
||||
return { context in
|
||||
let content = content.update(
|
||||
component: context.component.content,
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 44.0), transition: context.transition
|
||||
)
|
||||
|
||||
let size = CGSize(width: content.size.width + context.component.insets.left + context.component.insets.right, height: content.size.height + context.component.insets.top + context.component.insets.bottom)
|
||||
|
||||
let component = context.component
|
||||
|
||||
context.add(content
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
.opacity(context.state.isHighlighted ? 0.2 : 1.0)
|
||||
.update(Transition.Update { component, view, transition in
|
||||
view.frame = component.size.centered(around: component._position ?? CGPoint())
|
||||
})
|
||||
.gesture(.tap {
|
||||
component.action()
|
||||
})
|
||||
)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
48
submodules/ComponentFlow/Source/Components/List.swift
Normal file
48
submodules/ComponentFlow/Source/Components/List.swift
Normal file
@ -0,0 +1,48 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class List<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
public typealias EnvironmentType = ChildEnvironment
|
||||
|
||||
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
||||
private let appear: Transition.Appear
|
||||
|
||||
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], appear: Transition.Appear = .default()) {
|
||||
self.items = items
|
||||
self.appear = appear
|
||||
}
|
||||
|
||||
public static func ==(lhs: List<ChildEnvironment>, rhs: List<ChildEnvironment>) -> Bool {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static var body: Body {
|
||||
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
|
||||
|
||||
return { context in
|
||||
let updatedChildren = context.component.items.map { item in
|
||||
return children[item.id].update(
|
||||
component: item.component, environment: {
|
||||
context.environment[ChildEnvironment.self]
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
}
|
||||
|
||||
var nextOrigin: CGFloat = 0.0
|
||||
for child in updatedChildren {
|
||||
context.add(child
|
||||
.position(CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0))
|
||||
.appear(context.component.appear)
|
||||
)
|
||||
nextOrigin += child.size.height
|
||||
}
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
41
submodules/ComponentFlow/Source/Components/Rectangle.swift
Normal file
41
submodules/ComponentFlow/Source/Components/Rectangle.swift
Normal file
@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class Rectangle: Component {
|
||||
private let color: UIColor
|
||||
private let width: CGFloat?
|
||||
private let height: CGFloat?
|
||||
|
||||
public init(color: UIColor, width: CGFloat? = nil, height: CGFloat? = nil) {
|
||||
self.color = color
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
|
||||
public static func ==(lhs: Rectangle, rhs: Rectangle) -> Bool {
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
if lhs.width != rhs.width {
|
||||
return false
|
||||
}
|
||||
if lhs.height != rhs.height {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
var size = availableSize
|
||||
if let width = self.width {
|
||||
size.width = min(size.width, width)
|
||||
}
|
||||
if let height = self.height {
|
||||
size.height = min(size.height, height)
|
||||
}
|
||||
|
||||
view.backgroundColor = self.color
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
101
submodules/ComponentFlow/Source/Components/Text.swift
Normal file
101
submodules/ComponentFlow/Source/Components/Text.swift
Normal file
@ -0,0 +1,101 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class Text: Component {
|
||||
private final class MeasureState: Equatable {
|
||||
let attributedText: NSAttributedString
|
||||
let availableSize: CGSize
|
||||
let size: CGSize
|
||||
|
||||
init(attributedText: NSAttributedString, availableSize: CGSize, size: CGSize) {
|
||||
self.attributedText = attributedText
|
||||
self.availableSize = availableSize
|
||||
self.size = size
|
||||
}
|
||||
|
||||
static func ==(lhs: MeasureState, rhs: MeasureState) -> Bool {
|
||||
if !lhs.attributedText.isEqual(rhs.attributedText) {
|
||||
return false
|
||||
}
|
||||
if lhs.availableSize != rhs.availableSize {
|
||||
return false
|
||||
}
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var measureState: MeasureState?
|
||||
|
||||
func update(component: Text, availableSize: CGSize) -> CGSize {
|
||||
let attributedText = NSAttributedString(string: component.text, attributes: [
|
||||
NSAttributedString.Key.font: component.font,
|
||||
NSAttributedString.Key.foregroundColor: component.color
|
||||
])
|
||||
|
||||
if let measureState = self.measureState {
|
||||
if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize {
|
||||
return measureState.size
|
||||
}
|
||||
}
|
||||
|
||||
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
boundingRect.size.width = ceil(boundingRect.size.width)
|
||||
boundingRect.size.height = ceil(boundingRect.size.height)
|
||||
|
||||
let measureState = MeasureState(attributedText: attributedText, availableSize: availableSize, size: boundingRect.size)
|
||||
if #available(iOS 10.0, *) {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: measureState.size))
|
||||
let image = renderer.image { context in
|
||||
UIGraphicsPushContext(context.cgContext)
|
||||
measureState.attributedText.draw(at: CGPoint())
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
self.layer.contents = image.cgImage
|
||||
} else {
|
||||
UIGraphicsBeginImageContextWithOptions(measureState.size, false, 0.0)
|
||||
measureState.attributedText.draw(at: CGPoint())
|
||||
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
self.measureState = measureState
|
||||
|
||||
return boundingRect.size
|
||||
}
|
||||
}
|
||||
|
||||
public let text: String
|
||||
public let font: UIFont
|
||||
public let color: UIColor
|
||||
|
||||
public init(text: String, font: UIFont, color: UIColor) {
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.color = color
|
||||
}
|
||||
|
||||
public static func ==(lhs: Text, rhs: Text) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if !lhs.font.isEqual(rhs.font) {
|
||||
return false
|
||||
}
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize)
|
||||
}
|
||||
}
|
29
submodules/ComponentFlow/Source/Gestures/Gesture.swift
Normal file
29
submodules/ComponentFlow/Source/Gestures/Gesture.swift
Normal file
@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public class Gesture {
|
||||
class Id {
|
||||
private var _id: UInt = 0
|
||||
public var id: UInt {
|
||||
return self._id
|
||||
}
|
||||
|
||||
init() {
|
||||
self._id = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
}
|
||||
|
||||
let id: Id
|
||||
|
||||
init(id: Id) {
|
||||
self.id = id
|
||||
}
|
||||
|
||||
func create() -> UIGestureRecognizer {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(gesture: UIGestureRecognizer) {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
59
submodules/ComponentFlow/Source/Gestures/PanGesture.swift
Normal file
59
submodules/ComponentFlow/Source/Gestures/PanGesture.swift
Normal file
@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension Gesture {
|
||||
enum PanGestureState {
|
||||
case began
|
||||
case updated(offset: CGPoint)
|
||||
case ended
|
||||
}
|
||||
|
||||
private final class PanGesture: Gesture {
|
||||
private class Impl: UIPanGestureRecognizer {
|
||||
var action: (PanGestureState) -> Void
|
||||
|
||||
init(action: @escaping (PanGestureState) -> Void) {
|
||||
self.action = action
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
self.addTarget(self, action: #selector(self.onAction))
|
||||
}
|
||||
|
||||
@objc private func onAction() {
|
||||
switch self.state {
|
||||
case .began:
|
||||
self.action(.began)
|
||||
case .ended, .cancelled:
|
||||
self.action(.ended)
|
||||
case .changed:
|
||||
let offset = self.translation(in: self.view)
|
||||
self.action(.updated(offset: offset))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static let id = Id()
|
||||
|
||||
private let action: (PanGestureState) -> Void
|
||||
|
||||
init(action: @escaping (PanGestureState) -> Void) {
|
||||
self.action = action
|
||||
|
||||
super.init(id: Self.id)
|
||||
}
|
||||
|
||||
override func create() -> UIGestureRecognizer {
|
||||
return Impl(action: self.action)
|
||||
}
|
||||
|
||||
override func update(gesture: UIGestureRecognizer) {
|
||||
(gesture as! Impl).action = action
|
||||
}
|
||||
}
|
||||
|
||||
static func pan(_ action: @escaping (PanGestureState) -> Void) -> Gesture {
|
||||
return PanGesture(action: action)
|
||||
}
|
||||
}
|
43
submodules/ComponentFlow/Source/Gestures/TapGesture.swift
Normal file
43
submodules/ComponentFlow/Source/Gestures/TapGesture.swift
Normal file
@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension Gesture {
|
||||
private final class TapGesture: Gesture {
|
||||
private class Impl: UITapGestureRecognizer {
|
||||
var action: () -> Void
|
||||
|
||||
init(action: @escaping () -> Void) {
|
||||
self.action = action
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
self.addTarget(self, action: #selector(self.onAction))
|
||||
}
|
||||
|
||||
@objc private func onAction() {
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
static let id = Id()
|
||||
|
||||
private let action: () -> Void
|
||||
|
||||
init(action: @escaping () -> Void) {
|
||||
self.action = action
|
||||
|
||||
super.init(id: Self.id)
|
||||
}
|
||||
|
||||
override func create() -> UIGestureRecognizer {
|
||||
return Impl(action: self.action)
|
||||
}
|
||||
|
||||
override func update(gesture: UIGestureRecognizer) {
|
||||
(gesture as! Impl).action = action
|
||||
}
|
||||
}
|
||||
|
||||
static func tap(_ action: @escaping () -> Void) -> Gesture {
|
||||
return TapGesture(action: action)
|
||||
}
|
||||
}
|
69
submodules/ComponentFlow/Source/Host/ComponentHostView.swift
Normal file
69
submodules/ComponentFlow/Source/Host/ComponentHostView.swift
Normal file
@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class ComponentHostView<EnvironmentType>: UIView {
|
||||
private var componentView: UIView?
|
||||
private(set) var isUpdating: Bool = false
|
||||
|
||||
public init() {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, containerSize: CGSize) -> CGSize {
|
||||
self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, containerSize: containerSize)
|
||||
}
|
||||
|
||||
private func _update(transition: Transition, component: AnyComponent<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, containerSize: CGSize) -> CGSize {
|
||||
precondition(!self.isUpdating)
|
||||
self.isUpdating = true
|
||||
|
||||
precondition(containerSize.width.isFinite)
|
||||
precondition(containerSize.width < .greatestFiniteMagnitude)
|
||||
precondition(containerSize.height.isFinite)
|
||||
precondition(containerSize.height < .greatestFiniteMagnitude)
|
||||
|
||||
let componentView: UIView
|
||||
if let current = self.componentView {
|
||||
componentView = current
|
||||
} else {
|
||||
componentView = component._makeView()
|
||||
self.componentView = componentView
|
||||
self.addSubview(componentView)
|
||||
}
|
||||
|
||||
let context = componentView.context(component: component)
|
||||
|
||||
let componentState: ComponentState = context.erasedState
|
||||
|
||||
if updateEnvironment {
|
||||
EnvironmentBuilder._environment = context.erasedEnvironment
|
||||
let _ = maybeEnvironment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
}
|
||||
|
||||
componentState._updated = { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {
|
||||
preconditionFailure()
|
||||
} as () -> Environment<EnvironmentType>, updateEnvironment: false, containerSize: containerSize)
|
||||
}
|
||||
|
||||
let updatedSize = component._update(view: componentView, availableSize: containerSize, transition: transition)
|
||||
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize))
|
||||
|
||||
self.isUpdating = false
|
||||
|
||||
return updatedSize
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public struct NavigationLayout: Equatable {
|
||||
public var statusBarHeight: CGFloat
|
||||
public var inputHeight: CGFloat
|
||||
public var bottomNavigationHeight: CGFloat
|
||||
}
|
134
submodules/ComponentFlow/Source/Host/RootHostView.swift
Normal file
134
submodules/ComponentFlow/Source/Host/RootHostView.swift
Normal file
@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class RootHostView<EnvironmentType: Equatable>: UIViewController {
|
||||
private let content: AnyComponent<(NavigationLayout, EnvironmentType)>
|
||||
|
||||
private var keyboardWillChangeFrameObserver: NSObjectProtocol?
|
||||
private var inputHeight: CGFloat = 0.0
|
||||
|
||||
private let environment: Environment<EnvironmentType>
|
||||
private var componentView: ComponentHostView<(NavigationLayout, EnvironmentType)>
|
||||
|
||||
private var scheduledTransition: Transition?
|
||||
|
||||
public init(
|
||||
content: AnyComponent<(NavigationLayout, EnvironmentType)>,
|
||||
@EnvironmentBuilder environment: () -> Environment<EnvironmentType>
|
||||
) {
|
||||
self.content = content
|
||||
|
||||
self.environment = Environment<EnvironmentType>()
|
||||
self.componentView = ComponentHostView<(NavigationLayout, EnvironmentType)>()
|
||||
|
||||
EnvironmentBuilder._environment = self.environment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(forName: UIApplication.keyboardWillChangeFrameNotification, object: nil, queue: nil, using: { [weak self] notification in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard let keyboardFrame = notification.userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect else {
|
||||
return
|
||||
}
|
||||
|
||||
var duration: Double = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0
|
||||
if duration > Double.ulpOfOne {
|
||||
duration = 0.5
|
||||
}
|
||||
let curve: UInt = (notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7
|
||||
|
||||
let transition: Transition
|
||||
if curve == 7 {
|
||||
transition = Transition(animation: .curve(duration: duration, curve: .spring))
|
||||
} else {
|
||||
transition = Transition(animation: .curve(duration: duration, curve: .easeInOut))
|
||||
}
|
||||
|
||||
strongSelf.updateKeyboardLayout(keyboardFrame: keyboardFrame, transition: transition)
|
||||
})
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.keyboardWillChangeFrameObserver.flatMap(NotificationCenter.default.removeObserver)
|
||||
}
|
||||
|
||||
private func updateKeyboardLayout(keyboardFrame: CGRect, transition: Transition) {
|
||||
self.inputHeight = max(0.0, self.view.bounds.height - keyboardFrame.minY)
|
||||
if self.componentView.isUpdating || true {
|
||||
if let _ = self.scheduledTransition {
|
||||
if case .curve = transition.animation {
|
||||
self.scheduledTransition = transition
|
||||
}
|
||||
} else {
|
||||
self.scheduledTransition = transition
|
||||
}
|
||||
self.view.setNeedsLayout()
|
||||
} else {
|
||||
self.updateComponent(size: self.view.bounds.size, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateComponent(size: CGSize, transition: Transition) {
|
||||
self.environment._isUpdated = false
|
||||
|
||||
transition.setFrame(view: self.componentView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.componentView.update(
|
||||
transition: transition,
|
||||
component: self.content,
|
||||
environment: {
|
||||
NavigationLayout(
|
||||
statusBarHeight: size.width > size.height ? 0.0 : 40.0,
|
||||
inputHeight: self.inputHeight,
|
||||
bottomNavigationHeight: 22.0
|
||||
)
|
||||
self.environment[EnvironmentType.self]
|
||||
},
|
||||
containerSize: size
|
||||
)
|
||||
}
|
||||
|
||||
public func updateEnvironment(@EnvironmentBuilder environment: () -> Environment<EnvironmentType>) {
|
||||
EnvironmentBuilder._environment = self.environment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
if self.environment.calculateIsUpdated() {
|
||||
if !self.view.bounds.size.width.isZero {
|
||||
self.updateComponent(size: self.view.bounds.size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.addSubview(self.componentView)
|
||||
|
||||
if !self.view.bounds.size.width.isZero {
|
||||
self.updateComponent(size: self.view.bounds.size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
if let scheduledTransition = self.scheduledTransition {
|
||||
self.scheduledTransition = nil
|
||||
self.updateComponent(size: self.view.bounds.size, transition: scheduledTransition)
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
self.updateComponent(size: size, transition: coordinator.isAnimated ? .easeInOut(duration: 0.3) : .immediate)
|
||||
}
|
||||
}
|
8
submodules/ComponentFlow/Source/Utils/Color.swift
Normal file
8
submodules/ComponentFlow/Source/Utils/Color.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
convenience init(rgb: UInt32) {
|
||||
self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
9
submodules/ComponentFlow/Source/Utils/Condition.swift
Normal file
9
submodules/ComponentFlow/Source/Utils/Condition.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
public func Condition<R>(_ f: @autoclosure () -> Bool, _ pass: () -> R) -> R? {
|
||||
if f() {
|
||||
return pass()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
13
submodules/ComponentFlow/Source/Utils/EscapeGuard.swift
Normal file
13
submodules/ComponentFlow/Source/Utils/EscapeGuard.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
final class EscapeGuard {
|
||||
final class Status {
|
||||
fileprivate(set) var isDeallocated: Bool = false
|
||||
}
|
||||
|
||||
let status = Status()
|
||||
|
||||
deinit {
|
||||
self.status.isDeallocated = true
|
||||
}
|
||||
}
|
8
submodules/ComponentFlow/Source/Utils/Insets.swift
Normal file
8
submodules/ComponentFlow/Source/Utils/Insets.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension UIEdgeInsets {
|
||||
init(_ value: CGFloat) {
|
||||
self.init(top: value, left: value, bottom: value, right: value)
|
||||
}
|
||||
}
|
8
submodules/ComponentFlow/Source/Utils/Rect.swift
Normal file
8
submodules/ComponentFlow/Source/Utils/Rect.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension CGRect {
|
||||
var center: CGPoint {
|
||||
return CGPoint(x: self.midX, y: self.midY)
|
||||
}
|
||||
}
|
28
submodules/ComponentFlow/Source/Utils/Size.swift
Normal file
28
submodules/ComponentFlow/Source/Utils/Size.swift
Normal file
@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension CGSize {
|
||||
func centered(in rect: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self)
|
||||
}
|
||||
|
||||
func centered(around position: CGPoint) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: position.x - self.width / 2.0, y: position.y - self.height / 2.0), size: self)
|
||||
}
|
||||
|
||||
func leftCentered(in rect: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: rect.minX, y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self)
|
||||
}
|
||||
|
||||
func rightCentered(in rect: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: rect.maxX - self.width, y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self)
|
||||
}
|
||||
|
||||
func topCentered(in rect: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: 0.0), size: self)
|
||||
}
|
||||
|
||||
func bottomCentered(in rect: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: rect.maxY - self.height), size: self)
|
||||
}
|
||||
}
|
@ -396,8 +396,8 @@ public enum TabBarItemContextActionType {
|
||||
}
|
||||
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition)
|
||||
if !transition.isAnimated {
|
||||
navigationBar.layer.cancelAnimationsRecursive(key: "bounds")
|
||||
navigationBar.layer.cancelAnimationsRecursive(key: "position")
|
||||
navigationBar.layer.removeAnimation(forKey: "bounds")
|
||||
navigationBar.layer.removeAnimation(forKey: "position")
|
||||
}
|
||||
transition.updateFrame(node: navigationBar, frame: navigationBarFrame)
|
||||
navigationBar.setHidden(!self.displayNavigationBar, animated: transition.isAnimated)
|
||||
|
@ -215,7 +215,7 @@ public final class ReadBuffer: MemoryBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
private enum ValueType: Int8 {
|
||||
enum ValueType: Int8 {
|
||||
case Int32 = 0
|
||||
case Int64 = 1
|
||||
case Bool = 2
|
||||
@ -258,35 +258,21 @@ public final class PostboxEncoder {
|
||||
self.buffer.reset()
|
||||
}
|
||||
|
||||
public func encodeKey(_ key: StaticString) {
|
||||
var length: Int8 = Int8(key.utf8CodeUnitCount)
|
||||
self.buffer.write(&length, offset: 0, length: 1)
|
||||
self.buffer.write(key.utf8Start, offset: 0, length: Int(length))
|
||||
}
|
||||
|
||||
public func encodeKey(_ key: String) {
|
||||
let data = key.data(using: .utf8)!
|
||||
data.withUnsafeBytes { (keyBytes: UnsafePointer<UInt8>) -> Void in
|
||||
var length: Int8 = Int8(data.count)
|
||||
self.buffer.write(&length, offset: 0, length: 1)
|
||||
self.buffer.write(keyBytes, offset: 0, length: Int(length))
|
||||
var length: Int8 = Int8(data.count)
|
||||
self.buffer.write(&length, offset: 0, length: 1)
|
||||
data.withUnsafeBytes { bytes in
|
||||
self.buffer.write(bytes.baseAddress!, offset: 0, length: Int(length))
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeNil(forKey key: StaticString) {
|
||||
public func encodeNil(forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Nil.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
}
|
||||
|
||||
public func encodeInt32(_ value: Int32, forKey key: StaticString) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Int32.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
var v = value
|
||||
self.buffer.write(&v, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
public func encodeInt32(_ value: Int32, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Int32.rawValue
|
||||
@ -295,7 +281,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(&v, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
public func encodeInt64(_ value: Int64, forKey key: StaticString) {
|
||||
public func encodeInt64(_ value: Int64, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Int64.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -303,7 +289,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(&v, offset: 0, length: 8)
|
||||
}
|
||||
|
||||
public func encodeBool(_ value: Bool, forKey key: StaticString) {
|
||||
public func encodeBool(_ value: Bool, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Bool.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -311,7 +297,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(&v, offset: 0, length: 1)
|
||||
}
|
||||
|
||||
public func encodeDouble(_ value: Double, forKey key: StaticString) {
|
||||
public func encodeDouble(_ value: Double, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Double.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -319,7 +305,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(&v, offset: 0, length: 8)
|
||||
}
|
||||
|
||||
public func encodeString(_ value: String, forKey key: StaticString) {
|
||||
public func encodeString(_ value: String, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.String.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -337,13 +323,13 @@ public final class PostboxEncoder {
|
||||
self.encodeObject(value, forKey: "_")
|
||||
}
|
||||
|
||||
public func encodeCodable<T: Codable>(_ value: T, forKey key: StaticString) {
|
||||
public func encodeCodable<T: Codable>(_ value: T, forKey key: String) {
|
||||
if let data = try? JSONEncoder().encode(value) {
|
||||
self.encodeData(data, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeObject(_ value: PostboxCoding, forKey key: StaticString) {
|
||||
public func encodeObject(_ value: PostboxCoding, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var t: Int8 = ValueType.Object.rawValue
|
||||
self.buffer.write(&t, offset: 0, length: 1)
|
||||
@ -377,7 +363,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(innerEncoder.buffer.memory, offset: 0, length: Int(length))
|
||||
}
|
||||
|
||||
public func encodeInt32Array(_ value: [Int32], forKey key: StaticString) {
|
||||
public func encodeInt32Array(_ value: [Int32], forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Int32Array.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -389,7 +375,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeInt64Array(_ value: [Int64], forKey key: StaticString) {
|
||||
public func encodeInt64Array(_ value: [Int64], forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Int64Array.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -401,7 +387,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeObjectArray<T: PostboxCoding>(_ value: [T], forKey key: StaticString) {
|
||||
public func encodeObjectArray<T: PostboxCoding>(_ value: [T], forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var t: Int8 = ValueType.ObjectArray.rawValue
|
||||
self.buffer.write(&t, offset: 0, length: 1)
|
||||
@ -421,7 +407,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeObjectArrayWithEncoder<T>(_ value: [T], forKey key: StaticString, encoder: (T, PostboxEncoder) -> Void) {
|
||||
public func encodeObjectArrayWithEncoder<T>(_ value: [T], forKey key: String, encoder: (T, PostboxEncoder) -> Void) {
|
||||
self.encodeKey(key)
|
||||
var t: Int8 = ValueType.ObjectArray.rawValue
|
||||
self.buffer.write(&t, offset: 0, length: 1)
|
||||
@ -441,7 +427,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeGenericObjectArray(_ value: [PostboxCoding], forKey key: StaticString) {
|
||||
public func encodeGenericObjectArray(_ value: [PostboxCoding], forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var t: Int8 = ValueType.ObjectArray.rawValue
|
||||
self.buffer.write(&t, offset: 0, length: 1)
|
||||
@ -461,7 +447,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeStringArray(_ value: [String], forKey key: StaticString) {
|
||||
public func encodeStringArray(_ value: [String], forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.StringArray.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -476,7 +462,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeBytesArray(_ value: [MemoryBuffer], forKey key: StaticString) {
|
||||
public func encodeBytesArray(_ value: [MemoryBuffer], forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.BytesArray.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -490,7 +476,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeDataArray(_ value: [Data], forKey key: StaticString) {
|
||||
public func encodeDataArray(_ value: [Data], forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.BytesArray.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -506,7 +492,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeObjectDictionary<K, V: PostboxCoding>(_ value: [K : V], forKey key: StaticString) where K: PostboxCoding {
|
||||
public func encodeObjectDictionary<K, V: PostboxCoding>(_ value: [K : V], forKey key: String) where K: PostboxCoding {
|
||||
self.encodeKey(key)
|
||||
var t: Int8 = ValueType.ObjectDictionary.rawValue
|
||||
self.buffer.write(&t, offset: 0, length: 1)
|
||||
@ -533,7 +519,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeObjectDictionary<K, V: PostboxCoding>(_ value: [K : V], forKey key: StaticString, keyEncoder: (K, PostboxEncoder) -> Void) {
|
||||
public func encodeObjectDictionary<K, V: PostboxCoding>(_ value: [K : V], forKey key: String, keyEncoder: (K, PostboxEncoder) -> Void) {
|
||||
self.encodeKey(key)
|
||||
var t: Int8 = ValueType.ObjectDictionary.rawValue
|
||||
self.buffer.write(&t, offset: 0, length: 1)
|
||||
@ -560,7 +546,7 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeBytes(_ bytes: WriteBuffer, forKey key: StaticString) {
|
||||
public func encodeBytes(_ bytes: WriteBuffer, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Bytes.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -569,7 +555,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(bytes.memory, offset: 0, length: bytes.offset)
|
||||
}
|
||||
|
||||
public func encodeBytes(_ bytes: ReadBuffer, forKey key: StaticString) {
|
||||
public func encodeBytes(_ bytes: ReadBuffer, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Bytes.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -578,7 +564,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(bytes.memory, offset: 0, length: bytes.offset)
|
||||
}
|
||||
|
||||
public func encodeBytes(_ bytes: MemoryBuffer, forKey key: StaticString) {
|
||||
public func encodeBytes(_ bytes: MemoryBuffer, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Bytes.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -587,7 +573,7 @@ public final class PostboxEncoder {
|
||||
self.buffer.write(bytes.memory, offset: 0, length: bytes.length)
|
||||
}
|
||||
|
||||
public func encodeData(_ data: Data, forKey key: StaticString) {
|
||||
public func encodeData(_ data: Data, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
var type: Int8 = ValueType.Bytes.rawValue
|
||||
self.buffer.write(&type, offset: 0, length: 1)
|
||||
@ -598,6 +584,24 @@ public final class PostboxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func encode<T: Encodable>(_ value: T, forKey key: String) {
|
||||
let typeHash: Int32 = murMurHashString32("\(type(of: value))")
|
||||
let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash)
|
||||
try! value.encode(to: innerEncoder)
|
||||
|
||||
let (data, valueType) = innerEncoder.makeData()
|
||||
self.encodeInnerObjectData(data, valueType: valueType, forKey: key)
|
||||
}
|
||||
|
||||
func encodeInnerObjectData(_ value: Data, valueType: ValueType, forKey key: String) {
|
||||
self.encodeKey(key)
|
||||
|
||||
var t: Int8 = valueType.rawValue
|
||||
self.buffer.write(&t, offset: 0, length: 1)
|
||||
|
||||
self.buffer.write(value)
|
||||
}
|
||||
|
||||
public let sharedWriteBuffer = WriteBuffer()
|
||||
}
|
||||
|
||||
@ -680,41 +684,65 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class func positionOnKey(_ rawBytes: UnsafeRawPointer, offset: inout Int, maxOffset: Int, length: Int, key: StaticString, valueType: ValueType) -> Bool
|
||||
|
||||
private class func positionOnKey(_ rawBytes: UnsafeRawPointer, offset: inout Int, maxOffset: Int, length: Int, key: String, valueType: ValueType) -> Bool {
|
||||
var actualValueType: ValueType = .Nil
|
||||
return positionOnKey(rawBytes, offset: &offset, maxOffset: maxOffset, length: length, key: key, valueType: valueType, actualValueType: &actualValueType, consumeKey: true)
|
||||
}
|
||||
|
||||
private class func positionOnKey(_ rawBytes: UnsafeRawPointer, offset: inout Int, maxOffset: Int, length: Int, key: String, valueType: ValueType?, actualValueType: inout ValueType, consumeKey: Bool) -> Bool
|
||||
{
|
||||
let bytes = rawBytes.assumingMemoryBound(to: Int8.self)
|
||||
|
||||
|
||||
let startOffset = offset
|
||||
|
||||
let keyLength: Int = key.utf8CodeUnitCount
|
||||
|
||||
let keyData = key.data(using: .utf8)!
|
||||
|
||||
let keyLength: Int = keyData.count
|
||||
while (offset < maxOffset) {
|
||||
let keyOffset = offset
|
||||
let readKeyLength = bytes[offset]
|
||||
assert(readKeyLength >= 0)
|
||||
offset += 1
|
||||
offset += Int(readKeyLength)
|
||||
|
||||
|
||||
let readValueType = bytes[offset]
|
||||
offset += 1
|
||||
|
||||
if keyLength == Int(readKeyLength) && memcmp(bytes + (offset - Int(readKeyLength) - 1), key.utf8Start, keyLength) == 0 {
|
||||
if readValueType == valueType.rawValue {
|
||||
return true
|
||||
} else if readValueType == ValueType.Nil.rawValue {
|
||||
return false
|
||||
|
||||
if keyLength != Int(readKeyLength) {
|
||||
skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!)
|
||||
continue
|
||||
}
|
||||
|
||||
if keyData.withUnsafeBytes({ keyBytes -> Bool in
|
||||
return memcmp(bytes + (offset - Int(readKeyLength) - 1), keyBytes.baseAddress!, keyLength) == 0
|
||||
}) {
|
||||
if let valueType = valueType {
|
||||
if readValueType == valueType.rawValue {
|
||||
actualValueType = valueType
|
||||
return true
|
||||
} else if readValueType == ValueType.Nil.rawValue {
|
||||
return false
|
||||
} else {
|
||||
skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!)
|
||||
}
|
||||
} else {
|
||||
skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!)
|
||||
if !consumeKey {
|
||||
offset = keyOffset
|
||||
}
|
||||
actualValueType = ValueType(rawValue: readValueType)!
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (startOffset != 0) {
|
||||
offset = 0
|
||||
return positionOnKey(bytes, offset: &offset, maxOffset: startOffset, length: length, key: key, valueType: valueType)
|
||||
return positionOnKey(bytes, offset: &offset, maxOffset: startOffset, length: length, key: key, valueType: valueType, actualValueType: &actualValueType, consumeKey: consumeKey)
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@ -789,20 +817,26 @@ public final class PostboxDecoder {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func decodeInt32ForKey(_ key: StaticString, orElse: Int32) -> Int32 {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) {
|
||||
var value: Int32 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
return value
|
||||
|
||||
public func containsKey(_ key: String) -> Bool {
|
||||
var actualValueType: ValueType = .Nil
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: nil, actualValueType: &actualValueType, consumeKey: false) {
|
||||
return true
|
||||
} else {
|
||||
return orElse
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeNilForKey(_ key: String) -> Bool {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Nil) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeInt32ForKey(_ key: String, orElse: Int32) -> Int32 {
|
||||
if PostboxDecoder.positionOnStringKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) {
|
||||
var value: Int32 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
@ -812,7 +846,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeOptionalInt32ForKey(_ key: StaticString) -> Int32? {
|
||||
public func decodeOptionalInt32ForKey(_ key: String) -> Int32? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) {
|
||||
var value: Int32 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 4)
|
||||
@ -823,18 +857,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeOptionalInt32ForKey(_ key: String) -> Int32? {
|
||||
if PostboxDecoder.positionOnStringKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) {
|
||||
var value: Int32 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
return value
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeInt64ForKey(_ key: StaticString, orElse: Int64) -> Int64 {
|
||||
public func decodeInt64ForKey(_ key: String, orElse: Int64) -> Int64 {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int64) {
|
||||
var value: Int64 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 8)
|
||||
@ -845,7 +868,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeOptionalInt64ForKey(_ key: StaticString) -> Int64? {
|
||||
public func decodeOptionalInt64ForKey(_ key: String) -> Int64? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int64) {
|
||||
var value: Int64 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 8)
|
||||
@ -856,7 +879,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeBoolForKey(_ key: StaticString, orElse: Bool) -> Bool {
|
||||
public func decodeBoolForKey(_ key: String, orElse: Bool) -> Bool {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bool) {
|
||||
var value: Int8 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 1)
|
||||
@ -867,7 +890,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeOptionalBoolForKey(_ key: StaticString) -> Bool? {
|
||||
public func decodeOptionalBoolForKey(_ key: String) -> Bool? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bool) {
|
||||
var value: Int8 = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 1)
|
||||
@ -878,7 +901,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeDoubleForKey(_ key: StaticString, orElse: Double) -> Double {
|
||||
public func decodeDoubleForKey(_ key: String, orElse: Double) -> Double {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Double) {
|
||||
var value: Double = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 8)
|
||||
@ -889,7 +912,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeOptionalDoubleForKey(_ key: StaticString) -> Double? {
|
||||
public func decodeOptionalDoubleForKey(_ key: String) -> Double? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Double) {
|
||||
var value: Double = 0
|
||||
memcpy(&value, self.buffer.memory + self.offset, 8)
|
||||
@ -900,7 +923,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeStringForKey(_ key: StaticString, orElse: String) -> String {
|
||||
public func decodeStringForKey(_ key: String, orElse: String) -> String {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .String) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -912,7 +935,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeOptionalStringForKey(_ key: StaticString) -> String? {
|
||||
public func decodeOptionalStringForKey(_ key: String) -> String? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .String) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -928,7 +951,7 @@ public final class PostboxDecoder {
|
||||
return self.decodeObjectForKey("_")
|
||||
}
|
||||
|
||||
public func decodeCodable<T: Codable>(_ type: T.Type, forKey key: StaticString) -> T? {
|
||||
public func decodeCodable<T: Codable>(_ type: T.Type, forKey key: String) -> T? {
|
||||
if let data = self.decodeDataForKey(key) {
|
||||
return try? JSONDecoder().decode(T.self, from: data)
|
||||
} else {
|
||||
@ -936,7 +959,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectForKey(_ key: StaticString) -> PostboxCoding? {
|
||||
public func decodeObjectForKey(_ key: String) -> PostboxCoding? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) {
|
||||
var typeHash: Int32 = 0
|
||||
memcpy(&typeHash, self.buffer.memory + self.offset, 4)
|
||||
@ -953,8 +976,35 @@ public final class PostboxDecoder {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeObjectDataForKey(_ key: String) -> (Data, ValueType)? {
|
||||
var actualValueType: ValueType = .Nil
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: nil, actualValueType: &actualValueType, consumeKey: true) {
|
||||
if case .Object = actualValueType {
|
||||
self.offset += 4
|
||||
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
let innerData = ReadBuffer(memory: self.buffer.memory + self.offset, length: Int(length), freeWhenDone: false).makeData()
|
||||
self.offset += Int(length)
|
||||
|
||||
return (innerData, actualValueType)
|
||||
} else {
|
||||
let initialOffset = self.offset
|
||||
PostboxDecoder.skipValue(self.buffer.memory.assumingMemoryBound(to: Int8.self), offset: &self.offset, length: self.buffer.length, valueType: actualValueType)
|
||||
|
||||
let data = ReadBuffer(memory: UnsafeMutableRawPointer(mutating: self.buffer.memory.advanced(by: initialOffset)), length: self.offset - initialOffset, freeWhenDone: false).makeData()
|
||||
|
||||
return (data, actualValueType)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectForKey(_ key: StaticString, decoder: (PostboxDecoder) -> PostboxCoding) -> PostboxCoding? {
|
||||
public func decodeObjectForKey(_ key: String, decoder: (PostboxDecoder) -> PostboxCoding) -> PostboxCoding? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) {
|
||||
var typeHash: Int32 = 0
|
||||
memcpy(&typeHash, self.buffer.memory + self.offset, 4)
|
||||
@ -972,7 +1022,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeAnyObjectForKey(_ key: StaticString, decoder: (PostboxDecoder) -> Any?) -> Any? {
|
||||
public func decodeAnyObjectForKey(_ key: String, decoder: (PostboxDecoder) -> Any?) -> Any? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) {
|
||||
var typeHash: Int32 = 0
|
||||
memcpy(&typeHash, self.buffer.memory + self.offset, 4)
|
||||
@ -990,7 +1040,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectForKeyThrowing(_ key: StaticString, decoder: (PostboxDecoder) throws -> Any) throws -> Any? {
|
||||
public func decodeObjectForKeyThrowing(_ key: String, decoder: (PostboxDecoder) throws -> Any) throws -> Any? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) {
|
||||
var typeHash: Int32 = 0
|
||||
memcpy(&typeHash, self.buffer.memory + self.offset, 4)
|
||||
@ -1008,47 +1058,55 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeInt32ArrayForKey(_ key: StaticString) -> [Int32] {
|
||||
public func decodeInt32ArrayForKey(_ key: String) -> [Int32] {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32Array) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
var array: [Int32] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var element: Int32 = 0
|
||||
memcpy(&element, self.buffer.memory + (self.offset + 4 + 4 * Int(i)), 4)
|
||||
array.append(element)
|
||||
i += 1
|
||||
}
|
||||
self.offset += 4 + Int(length) * 4
|
||||
return array
|
||||
return decodeInt32ArrayRaw()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func decodeInt32ArrayRaw() -> [Int32] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
var array: [Int32] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var element: Int32 = 0
|
||||
memcpy(&element, self.buffer.memory + (self.offset + 4 + 4 * Int(i)), 4)
|
||||
array.append(element)
|
||||
i += 1
|
||||
}
|
||||
self.offset += 4 + Int(length) * 4
|
||||
return array
|
||||
}
|
||||
|
||||
public func decodeInt64ArrayForKey(_ key: StaticString) -> [Int64] {
|
||||
public func decodeInt64ArrayForKey(_ key: String) -> [Int64] {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int64Array) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
var array: [Int64] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var element: Int64 = 0
|
||||
memcpy(&element, self.buffer.memory + (self.offset + 4 + 8 * Int(i)), 8)
|
||||
array.append(element)
|
||||
i += 1
|
||||
}
|
||||
self.offset += 4 + Int(length) * 8
|
||||
return array
|
||||
return decodeInt64ArrayRaw()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func decodeInt64ArrayRaw() -> [Int64] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
var array: [Int64] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var element: Int64 = 0
|
||||
memcpy(&element, self.buffer.memory + (self.offset + 4 + 8 * Int(i)), 8)
|
||||
array.append(element)
|
||||
i += 1
|
||||
}
|
||||
self.offset += 4 + Int(length) * 8
|
||||
return array
|
||||
}
|
||||
|
||||
public func decodeObjectArrayWithDecoderForKey<T>(_ key: StaticString) -> [T] where T: PostboxCoding {
|
||||
public func decodeObjectArrayWithDecoderForKey<T>(_ key: String) -> [T] where T: PostboxCoding {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1080,7 +1138,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeOptionalObjectArrayWithDecoderForKey<T>(_ key: StaticString) -> [T]? where T: PostboxCoding {
|
||||
public func decodeOptionalObjectArrayWithDecoderForKey<T>(_ key: String) -> [T]? where T: PostboxCoding {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1108,11 +1166,20 @@ public final class PostboxDecoder {
|
||||
|
||||
return array
|
||||
} else {
|
||||
return nil
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32Array) {
|
||||
let array = decodeInt32ArrayRaw()
|
||||
if array.isEmpty {
|
||||
return []
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectArrayWithCustomDecoderForKey<T>(_ key: StaticString, decoder: (PostboxDecoder) throws -> T) throws -> [T] {
|
||||
public func decodeObjectArrayWithCustomDecoderForKey<T>(_ key: String, decoder: (PostboxDecoder) throws -> T) throws -> [T] {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1145,65 +1212,105 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeStringArrayForKey(_ key: StaticString) -> [String] {
|
||||
public func decodeStringArrayForKey(_ key: String) -> [String] {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .StringArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
var array: [String] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
let data = Data(bytes: self.buffer.memory.assumingMemoryBound(to: UInt8.self).advanced(by: self.offset + 4), count: Int(length))
|
||||
self.offset += 4 + Int(length)
|
||||
if let string = String(data: data, encoding: .utf8) {
|
||||
array.append(string)
|
||||
} else {
|
||||
assertionFailure()
|
||||
array.append("")
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
return array
|
||||
return decodeStringArrayRaw()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeStringArrayRaw() -> [String] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
var array: [String] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
let data = Data(bytes: self.buffer.memory.assumingMemoryBound(to: UInt8.self).advanced(by: self.offset + 4), count: Int(length))
|
||||
self.offset += 4 + Int(length)
|
||||
if let string = String(data: data, encoding: .utf8) {
|
||||
array.append(string)
|
||||
} else {
|
||||
assertionFailure()
|
||||
array.append("")
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
public func decodeBytesArrayForKey(_ key: StaticString) -> [MemoryBuffer] {
|
||||
public func decodeBytesArrayForKey(_ key: String) -> [MemoryBuffer] {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .BytesArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
var array: [MemoryBuffer] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
let bytes = malloc(Int(length))!
|
||||
memcpy(bytes, self.buffer.memory.advanced(by: self.offset + 4), Int(length))
|
||||
array.append(MemoryBuffer(memory: bytes, capacity: Int(length), length: Int(length), freeWhenDone: true))
|
||||
self.offset += 4 + Int(length)
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
return array
|
||||
return decodeBytesArrayRaw()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func decodeBytesArrayRaw() -> [MemoryBuffer] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
var array: [MemoryBuffer] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
let bytes = malloc(Int(length))!
|
||||
memcpy(bytes, self.buffer.memory.advanced(by: self.offset + 4), Int(length))
|
||||
array.append(MemoryBuffer(memory: bytes, capacity: Int(length), length: Int(length), freeWhenDone: true))
|
||||
self.offset += 4 + Int(length)
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
func decodeObjectDataArrayRaw() -> [Data] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
var array: [Data] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
|
||||
var i: Int32 = 0
|
||||
while i < length {
|
||||
var typeHash: Int32 = 0
|
||||
memcpy(&typeHash, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
var objectLength: Int32 = 0
|
||||
memcpy(&objectLength, self.buffer.memory + self.offset, 4)
|
||||
if objectLength < 0 || objectLength > 2 * 1024 * 1024 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
let innerBuffer = ReadBuffer(memory: self.buffer.memory + (self.offset + 4), length: Int(objectLength), freeWhenDone: false)
|
||||
let innerData = innerBuffer.makeData()
|
||||
self.offset += 4 + Int(objectLength)
|
||||
|
||||
array.append(innerData)
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
public func decodeOptionalDataArrayForKey(_ key: StaticString) -> [Data]? {
|
||||
public func decodeOptionalDataArrayForKey(_ key: String) -> [Data]? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .BytesArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1228,7 +1335,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectArrayForKey<T>(_ key: StaticString) -> [T] where T: PostboxCoding {
|
||||
public func decodeObjectArrayForKey<T>(_ key: String) -> [T] where T: PostboxCoding {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1271,7 +1378,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectArrayForKey(_ key: StaticString) -> [PostboxCoding] {
|
||||
public func decodeObjectArrayForKey(_ key: String) -> [PostboxCoding] {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1314,7 +1421,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectDictionaryForKey<K, V: PostboxCoding>(_ key: StaticString) -> [K : V] where K: PostboxCoding, K: Hashable {
|
||||
public func decodeObjectDictionaryForKey<K, V: PostboxCoding>(_ key: String) -> [K : V] where K: PostboxCoding, K: Hashable {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectDictionary) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1368,7 +1475,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectDictionaryForKey<K, V: PostboxCoding>(_ key: StaticString, keyDecoder: (PostboxDecoder) -> K) -> [K : V] where K: Hashable {
|
||||
public func decodeObjectDictionaryForKey<K, V: PostboxCoding>(_ key: String, keyDecoder: (PostboxDecoder) -> K) -> [K : V] where K: Hashable {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectDictionary) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1425,7 +1532,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeObjectDictionaryForKey<K, V: PostboxCoding>(_ key: StaticString, keyDecoder: (PostboxDecoder) -> K, valueDecoder: (PostboxDecoder) -> V) -> [K : V] where K: Hashable {
|
||||
public func decodeObjectDictionaryForKey<K, V: PostboxCoding>(_ key: String, keyDecoder: (PostboxDecoder) -> K, valueDecoder: (PostboxDecoder) -> V) -> [K : V] where K: Hashable {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectDictionary) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1482,7 +1589,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeBytesForKeyNoCopy(_ key: StaticString) -> ReadBuffer? {
|
||||
public func decodeBytesForKeyNoCopy(_ key: String) -> ReadBuffer? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bytes) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1493,7 +1600,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeBytesForKey(_ key: StaticString) -> ReadBuffer? {
|
||||
public func decodeBytesForKey(_ key: String) -> ReadBuffer? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bytes) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1506,7 +1613,7 @@ public final class PostboxDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeDataForKey(_ key: StaticString) -> Data? {
|
||||
public func decodeDataForKey(_ key: String) -> Data? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bytes) {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
@ -1520,4 +1627,23 @@ public final class PostboxDecoder {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func decode<T: Decodable>(_ type: T.Type, forKey key: String) -> T? {
|
||||
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) {
|
||||
var typeHash: Int32 = 0
|
||||
memcpy(&typeHash, self.buffer.memory + self.offset, 4)
|
||||
self.offset += 4
|
||||
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, self.buffer.memory + self.offset, 4)
|
||||
|
||||
let innerBuffer = ReadBuffer(memory: self.buffer.memory + (self.offset + 4), length: Int(length), freeWhenDone: false)
|
||||
let innerData = innerBuffer.makeData()
|
||||
self.offset += 4 + Int(length)
|
||||
|
||||
return try? AdaptedPostboxDecoder().decode(T.self, from: innerData)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
public protocol PeerChatListEmbeddedInterfaceState: PostboxCoding {
|
||||
public protocol PeerChatListEmbeddedInterfaceState: Codable {
|
||||
var timestamp: Int32 { get }
|
||||
|
||||
func isEqual(to: PeerChatListEmbeddedInterfaceState) -> Bool
|
||||
|
@ -0,0 +1,125 @@
|
||||
import Foundation
|
||||
|
||||
final public class AdaptedPostboxDecoder {
|
||||
enum ContentType {
|
||||
case object
|
||||
case int32Array
|
||||
case int64Array
|
||||
case objectArray
|
||||
case stringArray
|
||||
case dataArray
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
|
||||
return try self.decode(type, from: data, contentType: .object)
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, from data: Data, contentType: ContentType) throws -> T where T : Decodable {
|
||||
let decoder = _AdaptedPostboxDecoder(data: data, contentType: contentType)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
}
|
||||
|
||||
extension AdaptedPostboxDecoder.ContentType {
|
||||
init?(valueType: ValueType) {
|
||||
switch valueType {
|
||||
case .Int32:
|
||||
return nil
|
||||
case .Int64:
|
||||
return nil
|
||||
case .Bool:
|
||||
return nil
|
||||
case .Double:
|
||||
return nil
|
||||
case .String:
|
||||
return nil
|
||||
case .Object:
|
||||
self = .object
|
||||
case .Int32Array:
|
||||
self = .int32Array
|
||||
case .Int64Array:
|
||||
self = .int64Array
|
||||
case .ObjectArray:
|
||||
self = .objectArray
|
||||
case .ObjectDictionary:
|
||||
return nil
|
||||
case .Bytes:
|
||||
return nil
|
||||
case .Nil:
|
||||
return nil
|
||||
case .StringArray:
|
||||
self = .stringArray
|
||||
case .BytesArray:
|
||||
self = .dataArray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _AdaptedPostboxDecoder {
|
||||
var codingPath: [CodingKey] = []
|
||||
|
||||
var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
var container: AdaptedPostboxDecodingContainer?
|
||||
|
||||
fileprivate let data: Data
|
||||
fileprivate let contentType: AdaptedPostboxDecoder.ContentType
|
||||
|
||||
init(data: Data, contentType: AdaptedPostboxDecoder.ContentType) {
|
||||
self.data = data
|
||||
self.contentType = contentType
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxDecoder: Decoder {
|
||||
fileprivate func assertCanCreateContainer() {
|
||||
precondition(self.container == nil)
|
||||
}
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||
assertCanCreateContainer()
|
||||
|
||||
let container = KeyedContainer<Key>(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo)
|
||||
self.container = container
|
||||
|
||||
return KeyedDecodingContainer(container)
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> UnkeyedDecodingContainer {
|
||||
assertCanCreateContainer()
|
||||
|
||||
let decoder = PostboxDecoder(buffer: MemoryBuffer(data: self.data))
|
||||
|
||||
var content: UnkeyedContainer.Content?
|
||||
switch self.contentType {
|
||||
case .object:
|
||||
preconditionFailure()
|
||||
case .int32Array:
|
||||
content = .int32Array(decoder.decodeInt32ArrayRaw())
|
||||
case .int64Array:
|
||||
content = .int64Array(decoder.decodeInt64ArrayRaw())
|
||||
case .objectArray:
|
||||
content = .objectArray(decoder.decodeObjectDataArrayRaw())
|
||||
case .stringArray:
|
||||
content = .stringArray(decoder.decodeStringArrayRaw())
|
||||
case .dataArray:
|
||||
content = .dataArray(decoder.decodeBytesArrayRaw().map { $0.makeData() })
|
||||
}
|
||||
|
||||
if let content = content {
|
||||
let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, content: content)
|
||||
self.container = container
|
||||
|
||||
return container
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
func singleValueContainer() -> SingleValueDecodingContainer {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
protocol AdaptedPostboxDecodingContainer: AnyObject {
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
import Foundation
|
||||
|
||||
extension _AdaptedPostboxDecoder {
|
||||
final class KeyedContainer<Key> where Key: CodingKey {
|
||||
var codingPath: [CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any]
|
||||
let decoder: PostboxDecoder
|
||||
|
||||
init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
self.decoder = PostboxDecoder(buffer: MemoryBuffer(data: data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func decodingErrorBreakpoint() {
|
||||
#if DEBUG
|
||||
print("Decoding error. Install a breakpoint at decodingErrorBreakpoint to debug.")
|
||||
#endif
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
|
||||
var allKeys: [Key] {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func contains(_ key: Key) -> Bool {
|
||||
return self.decoder.containsKey(key.stringValue)
|
||||
}
|
||||
|
||||
func decodeNil(forKey key: Key) throws -> Bool {
|
||||
return self.decoder.decodeNilForKey(key.stringValue)
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
|
||||
if let (data, valueType) = self.decoder.decodeObjectDataForKey(key.stringValue) {
|
||||
if let mappedType = AdaptedPostboxDecoder.ContentType(valueType: valueType) {
|
||||
return try AdaptedPostboxDecoder().decode(T.self, from: data, contentType: mappedType)
|
||||
} else {
|
||||
decodingErrorBreakpoint()
|
||||
throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: ""))
|
||||
}
|
||||
} else {
|
||||
decodingErrorBreakpoint()
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
|
||||
if let value = self.decoder.decodeOptionalInt32ForKey(key.stringValue) {
|
||||
return value
|
||||
} else {
|
||||
decodingErrorBreakpoint()
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
|
||||
if let value = self.decoder.decodeOptionalInt64ForKey(key.stringValue) {
|
||||
return value
|
||||
} else {
|
||||
decodingErrorBreakpoint()
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
|
||||
if let value = self.decoder.decodeOptionalBoolForKey(key.stringValue) {
|
||||
return value
|
||||
} else {
|
||||
decodingErrorBreakpoint()
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func decode(_ type: String.Type, forKey key: Key) throws -> String {
|
||||
if let value = self.decoder.decodeOptionalStringForKey(key.stringValue) {
|
||||
return value
|
||||
} else {
|
||||
decodingErrorBreakpoint()
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func superDecoder() throws -> Decoder {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func superDecoder(forKey key: Key) throws -> Decoder {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxDecoder.KeyedContainer: AdaptedPostboxDecodingContainer {}
|
@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
|
||||
extension _AdaptedPostboxDecoder {
|
||||
final class SingleValueContainer {
|
||||
var codingPath: [CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
|
||||
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxDecoder.SingleValueContainer: SingleValueDecodingContainer {
|
||||
func decodeNil() -> Bool {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Bool.Type) throws -> Bool {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: String.Type) throws -> String {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Double.Type) throws -> Double {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Float.Type) throws -> Float {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Int.Type) throws -> Int {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Int8.Type) throws -> Int8 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Int16.Type) throws -> Int16 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Int32.Type) throws -> Int32 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: Int64.Type) throws -> Int64 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: UInt.Type) throws -> UInt {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxDecoder.SingleValueContainer: AdaptedPostboxDecodingContainer {}
|
@ -0,0 +1,129 @@
|
||||
import Foundation
|
||||
|
||||
extension _AdaptedPostboxDecoder {
|
||||
final class UnkeyedContainer {
|
||||
enum Content {
|
||||
case int32Array([Int32])
|
||||
case int64Array([Int64])
|
||||
case objectArray([Data])
|
||||
case stringArray([String])
|
||||
case dataArray([Data])
|
||||
|
||||
var count: Int {
|
||||
switch self {
|
||||
case let .int32Array(array):
|
||||
return array.count
|
||||
case let .int64Array(array):
|
||||
return array.count
|
||||
case let .objectArray(array):
|
||||
return array.count
|
||||
case let .stringArray(array):
|
||||
return array.count
|
||||
case let .dataArray(array):
|
||||
return array.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let codingPath: [CodingKey]
|
||||
let userInfo: [CodingUserInfoKey: Any]
|
||||
let content: Content
|
||||
|
||||
var count: Int? {
|
||||
return self.content.count
|
||||
}
|
||||
|
||||
var isAtEnd: Bool {
|
||||
return self.currentIndex >= self.content.count
|
||||
}
|
||||
|
||||
fileprivate var _currentIndex: Int = 0
|
||||
|
||||
var currentIndex: Int {
|
||||
return self._currentIndex
|
||||
}
|
||||
|
||||
init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey: Any], content: Content) {
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxDecoder.UnkeyedContainer: UnkeyedDecodingContainer {
|
||||
func decodeNil() throws -> Bool {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||
if type == Data.self {
|
||||
switch self.content {
|
||||
case let .dataArray(array):
|
||||
let index = self._currentIndex
|
||||
self._currentIndex += 1
|
||||
return array[index] as! T
|
||||
default:
|
||||
throw DecodingError.typeMismatch(Data.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: ""))
|
||||
}
|
||||
} else {
|
||||
switch self.content {
|
||||
case let .objectArray(array):
|
||||
let index = self._currentIndex
|
||||
self._currentIndex += 1
|
||||
|
||||
let data = array[index]
|
||||
return try AdaptedPostboxDecoder().decode(T.self, from: data)
|
||||
default:
|
||||
throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decode(_ type: Int32.Type) throws -> Int32 {
|
||||
switch self.content {
|
||||
case let .int32Array(array):
|
||||
let index = self._currentIndex
|
||||
self._currentIndex += 1
|
||||
return array[index]
|
||||
default:
|
||||
throw DecodingError.typeMismatch(Int32.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func decode(_ type: Int64.Type) throws -> Int64 {
|
||||
switch self.content {
|
||||
case let .int64Array(array):
|
||||
let index = self._currentIndex
|
||||
self._currentIndex += 1
|
||||
return array[index]
|
||||
default:
|
||||
throw DecodingError.typeMismatch(Int64.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func decode(_ type: String.Type) throws -> String {
|
||||
switch self.content {
|
||||
case let .stringArray(array):
|
||||
let index = self._currentIndex
|
||||
self._currentIndex += 1
|
||||
return array[index]
|
||||
default:
|
||||
throw DecodingError.typeMismatch(String.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func superDecoder() throws -> Decoder {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxDecoder.UnkeyedContainer: AdaptedPostboxDecodingContainer {}
|
@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
import MurMurHash32
|
||||
|
||||
public class AdaptedPostboxEncoder {
|
||||
func encode(_ value: Encodable) throws -> Data {
|
||||
let typeHash: Int32 = murMurHashString32("\(type(of: value))")
|
||||
|
||||
let encoder = _AdaptedPostboxEncoder(typeHash: typeHash)
|
||||
try value.encode(to: encoder)
|
||||
return encoder.makeData().0
|
||||
}
|
||||
}
|
||||
|
||||
final class _AdaptedPostboxEncoder {
|
||||
var codingPath: [CodingKey] = []
|
||||
|
||||
var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
let typeHash: Int32
|
||||
|
||||
fileprivate var container: AdaptedPostboxEncodingContainer?
|
||||
|
||||
init(typeHash: Int32) {
|
||||
self.typeHash = typeHash
|
||||
}
|
||||
|
||||
func makeData() -> (Data, ValueType) {
|
||||
return self.container!.makeData()
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxEncoder: Encoder {
|
||||
fileprivate func assertCanCreateContainer() {
|
||||
precondition(self.container == nil)
|
||||
}
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
|
||||
assertCanCreateContainer()
|
||||
|
||||
let container = KeyedContainer<Key>(codingPath: self.codingPath, userInfo: self.userInfo, typeHash: self.typeHash)
|
||||
self.container = container
|
||||
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
assertCanCreateContainer()
|
||||
|
||||
let container = UnkeyedContainer(codingPath: self.codingPath, userInfo: self.userInfo)
|
||||
self.container = container
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
protocol AdaptedPostboxEncodingContainer: AnyObject {
|
||||
func makeData() -> (Data, ValueType)
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
import MurMurHash32
|
||||
|
||||
extension _AdaptedPostboxEncoder {
|
||||
final class KeyedContainer<Key> where Key: CodingKey {
|
||||
var codingPath: [CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any]
|
||||
let typeHash: Int32
|
||||
|
||||
let encoder: PostboxEncoder
|
||||
|
||||
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], typeHash: Int32) {
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
self.typeHash = typeHash
|
||||
|
||||
self.encoder = PostboxEncoder()
|
||||
}
|
||||
|
||||
func makeData() -> (Data, ValueType) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var typeHash: Int32 = self.typeHash
|
||||
buffer.write(&typeHash, offset: 0, length: 4)
|
||||
|
||||
let data = self.encoder.makeData()
|
||||
|
||||
var length: Int32 = Int32(data.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
buffer.write(data)
|
||||
|
||||
return (buffer.makeData(), .Object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxEncoder.KeyedContainer: KeyedEncodingContainerProtocol {
|
||||
func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
|
||||
let typeHash: Int32 = murMurHashString32("\(type(of: value))")
|
||||
let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash)
|
||||
try! value.encode(to: innerEncoder)
|
||||
|
||||
let (data, valueType) = innerEncoder.makeData()
|
||||
self.encoder.encodeInnerObjectData(data, valueType: valueType, forKey: key.stringValue)
|
||||
}
|
||||
|
||||
func encodeNil(forKey key: Key) throws {
|
||||
self.encoder.encodeNil(forKey: key.stringValue)
|
||||
}
|
||||
|
||||
func encode(_ value: Int32, forKey key: Key) throws {
|
||||
self.encoder.encodeInt32(value, forKey: key.stringValue)
|
||||
}
|
||||
|
||||
func encode(_ value: Int64, forKey key: Key) throws {
|
||||
self.encoder.encodeInt64(value, forKey: key.stringValue)
|
||||
}
|
||||
|
||||
func encode(_ value: Bool, forKey key: Key) throws {
|
||||
self.encoder.encodeBool(value, forKey: key.stringValue)
|
||||
}
|
||||
|
||||
func encode(_ value: Double, forKey key: Key) throws {
|
||||
self.encoder.encodeDouble(value, forKey: key.stringValue)
|
||||
}
|
||||
|
||||
func encode(_ value: String, forKey key: Key) throws {
|
||||
self.encoder.encodeString(value, forKey: key.stringValue)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func superEncoder() -> Encoder {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func superEncoder(forKey key: Key) -> Encoder {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxEncoder.KeyedContainer: AdaptedPostboxEncodingContainer {}
|
@ -0,0 +1,85 @@
|
||||
import Foundation
|
||||
|
||||
extension _AdaptedPostboxEncoder {
|
||||
final class SingleValueContainer {
|
||||
var codingPath: [CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxEncoder.SingleValueContainer: SingleValueEncodingContainer {
|
||||
func encodeNil() throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Bool) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: String) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Double) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Float) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Int) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Int8) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Int16) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Int32) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: Int64) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt8) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt16) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt32) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt64) throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode<T>(_ value: T) throws where T : Encodable {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxEncoder.SingleValueContainer: AdaptedPostboxEncodingContainer {
|
||||
func makeData() -> (Data, ValueType) {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
import Foundation
|
||||
import MurMurHash32
|
||||
|
||||
extension _AdaptedPostboxEncoder {
|
||||
final class UnkeyedContainer {
|
||||
fileprivate enum Item {
|
||||
case int32(Int32)
|
||||
case int64(Int64)
|
||||
case string(String)
|
||||
case object(Data)
|
||||
case data(Data)
|
||||
}
|
||||
|
||||
let codingPath: [CodingKey]
|
||||
let userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
fileprivate var items: [Item] = []
|
||||
|
||||
var count: Int {
|
||||
return self.items.count
|
||||
}
|
||||
|
||||
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
func makeData() -> (Data, ValueType) {
|
||||
if self.items.isEmpty {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var length: Int32 = Int32(self.items.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
|
||||
return (buffer.makeData(), .Int32Array)
|
||||
} else if self.items.allSatisfy({ if case .int32 = $0 { return true } else { return false } }) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var length: Int32 = Int32(self.items.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
|
||||
for case .int32(var value) in self.items {
|
||||
buffer.write(&value, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
return (buffer.makeData(), .Int32Array)
|
||||
} else if self.items.allSatisfy({ if case .int64 = $0 { return true } else { return false } }) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var length: Int32 = Int32(self.items.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
|
||||
for case .int64(var value) in self.items {
|
||||
buffer.write(&value, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
return (buffer.makeData(), .Int64Array)
|
||||
} else if self.items.allSatisfy({ if case .string = $0 { return true } else { return false } }) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var length: Int32 = Int32(self.items.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
|
||||
for case .string(let value) in self.items {
|
||||
let data = value.data(using: .utf8, allowLossyConversion: true) ?? (String("").data(using: .utf8)!)
|
||||
var valueLength: Int32 = Int32(data.count)
|
||||
buffer.write(&valueLength, offset: 0, length: 4)
|
||||
buffer.write(data)
|
||||
}
|
||||
|
||||
return (buffer.makeData(), .StringArray)
|
||||
} else if self.items.allSatisfy({ if case .object = $0 { return true } else { return false } }) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var length: Int32 = Int32(self.items.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
|
||||
for case .object(let data) in self.items {
|
||||
buffer.write(data)
|
||||
}
|
||||
|
||||
return (buffer.makeData(), .ObjectArray)
|
||||
} else if self.items.allSatisfy({ if case .data = $0 { return true } else { return false } }) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var length: Int32 = Int32(self.items.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
|
||||
for case .data(let data) in self.items {
|
||||
var valueLength: Int32 = Int32(data.count)
|
||||
buffer.write(&valueLength, offset: 0, length: 4)
|
||||
buffer.write(data)
|
||||
}
|
||||
|
||||
return (buffer.makeData(), .BytesArray)
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
|
||||
func encodeNil() throws {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func encode<T>(_ value: T) throws where T : Encodable {
|
||||
let typeHash: Int32 = murMurHashString32("\(type(of: value))")
|
||||
|
||||
let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash)
|
||||
try! value.encode(to: innerEncoder)
|
||||
|
||||
let (data, _) = innerEncoder.makeData()
|
||||
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
buffer.write(data)
|
||||
|
||||
self.items.append(.object(buffer.makeData()))
|
||||
}
|
||||
|
||||
func encode(_ value: Int32) throws {
|
||||
self.items.append(.int32(value))
|
||||
}
|
||||
|
||||
func encode(_ value: Int64) throws {
|
||||
self.items.append(.int64(value))
|
||||
}
|
||||
|
||||
func encode(_ value: String) throws {
|
||||
self.items.append(.string(value))
|
||||
}
|
||||
|
||||
func encode(_ value: Data) throws {
|
||||
self.items.append(.data(value))
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func superEncoder() -> Encoder {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
extension _AdaptedPostboxEncoder.UnkeyedContainer: AdaptedPostboxEncodingContainer {
|
||||
func makeData() -> Data {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
|
@ -478,5 +478,35 @@ public extension TelegramEngine {
|
||||
public func updatePeerDescription(peerId: PeerId, description: String?) -> Signal<Void, UpdatePeerDescriptionError> {
|
||||
return _internal_updatePeerDescription(account: self.account, peerId: peerId, description: description)
|
||||
}
|
||||
|
||||
public func getNextUnreadChannel(peerId: PeerId) -> Signal<EnginePeer?, NoError> {
|
||||
return self.account.postbox.transaction { transaction -> EnginePeer? in
|
||||
var peers: [RenderedPeer] = []
|
||||
peers.append(contentsOf: transaction.getTopChatListEntries(groupId: .root, count: 100))
|
||||
peers.append(contentsOf: transaction.getTopChatListEntries(groupId: Namespaces.PeerGroup.archive, count: 100))
|
||||
|
||||
var results: [(EnginePeer, Int32)] = []
|
||||
|
||||
for peer in peers {
|
||||
guard let channel = peer.chatMainPeer as? TelegramChannel, case .broadcast = channel.info else {
|
||||
continue
|
||||
}
|
||||
if channel.id == peerId {
|
||||
continue
|
||||
}
|
||||
guard let readState = transaction.getCombinedPeerReadState(channel.id), readState.count != 0 else {
|
||||
continue
|
||||
}
|
||||
guard let topMessageIndex = transaction.getTopPeerMessageIndex(peerId: channel.id) else {
|
||||
continue
|
||||
}
|
||||
results.append((EnginePeer(channel), topMessageIndex.timestamp))
|
||||
}
|
||||
|
||||
results.sort(by: { $0.1 > $1.1 })
|
||||
|
||||
return results.first?.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
|
||||
public struct StringCodingKey: CodingKey, ExpressibleByStringLiteral {
|
||||
public var stringValue: String
|
||||
|
||||
public init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
|
||||
public init(_ stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
|
||||
public init(stringLiteral: String) {
|
||||
self.stringValue = stringLiteral
|
||||
}
|
||||
|
||||
public var intValue: Int? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public init?(intValue: Int) {
|
||||
return nil
|
||||
}
|
||||
}
|
@ -238,6 +238,7 @@ swift_library(
|
||||
"//submodules/ImportStickerPackUI:ImportStickerPackUI",
|
||||
"//submodules/GradientBackground:GradientBackground",
|
||||
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
@ -52,4 +52,33 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
|
||||
func onLayout() {
|
||||
}
|
||||
|
||||
final class SnapshotState {
|
||||
fileprivate let snapshotView: UIView
|
||||
|
||||
fileprivate init(snapshotView: UIView) {
|
||||
self.snapshotView = snapshotView
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSnapshotState() -> SnapshotState {
|
||||
let snapshotView = self.avatarNode.view.snapshotView(afterScreenUpdates: false)!
|
||||
return SnapshotState(
|
||||
snapshotView: snapshotView
|
||||
)
|
||||
}
|
||||
|
||||
func animateFromSnapshot(_ snapshotState: SnapshotState) {
|
||||
self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true)
|
||||
|
||||
snapshotState.snapshotView.frame = self.frame
|
||||
self.containerNode.view.addSubview(snapshotState.snapshotView)
|
||||
|
||||
let snapshotView = snapshotState.snapshotView
|
||||
snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
@ -454,6 +454,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
private var importStateDisposable: Disposable?
|
||||
|
||||
private var nextChannelToReadDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) {
|
||||
let _ = ChatControllerCount.modify { value in
|
||||
@ -3295,6 +3297,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedHasBotCommands(hasBotCommands).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages)
|
||||
.updatedAutoremoveTimeout(autoremoveTimeout)
|
||||
})
|
||||
|
||||
if let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info {
|
||||
if strongSelf.nextChannelToReadDisposable == nil {
|
||||
strongSelf.nextChannelToReadDisposable = (strongSelf.context.engine.peers.getNextUnreadChannel(peerId: channel.id)
|
||||
|> deliverOnMainQueue
|
||||
|> then(.complete() |> delay(1.0, queue: .mainQueue()))
|
||||
|> restart).start(next: { nextPeer in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.chatDisplayNode.historyNode.offerNextChannelToRead = true
|
||||
strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !strongSelf.didSetChatLocationInfoReady {
|
||||
strongSelf.didSetChatLocationInfoReady = true
|
||||
strongSelf._chatLocationInfoReady.set(.single(true))
|
||||
@ -3867,6 +3886,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.selectAddMemberDisposable.dispose()
|
||||
self.addMemberDisposable.dispose()
|
||||
self.importStateDisposable?.dispose()
|
||||
self.nextChannelToReadDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
|
||||
@ -7011,9 +7031,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.chatDisplayNode.historyNode.openNextChannelToRead = { [weak self] peer in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let navigationController = strongSelf.effectiveNavigationController {
|
||||
let snapshotState = strongSelf.chatDisplayNode.prepareSnapshotState(
|
||||
titleViewSnapshotState: strongSelf.chatTitleView?.prepareSnapshotState(),
|
||||
avatarSnapshotState: (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState()
|
||||
)
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: false, completion: { nextController in
|
||||
(nextController as! ChatControllerImpl).animateFromPreviousController(snapshotState: snapshotState)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
private var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState?
|
||||
|
||||
private func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) {
|
||||
self.storedAnimateFromSnapshotState = snapshotState
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
#if DEBUG
|
||||
@ -7067,7 +7108,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
self.didAppear = true
|
||||
|
||||
self.chatDisplayNode.historyNode.preloadPages = true
|
||||
self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false
|
||||
self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
|
||||
return a && b
|
||||
@ -7415,6 +7455,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return state.updatedInputMode({ _ in .text })
|
||||
})
|
||||
}
|
||||
|
||||
if let snapshotState = self.storedAnimateFromSnapshotState {
|
||||
self.storedAnimateFromSnapshotState = nil
|
||||
|
||||
if let titleViewSnapshotState = snapshotState.titleViewSnapshotState {
|
||||
self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState)
|
||||
}
|
||||
if let avatarSnapshotState = snapshotState.avatarSnapshotState {
|
||||
(self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState)
|
||||
}
|
||||
self.chatDisplayNode.animateFromSnapshot(snapshotState)
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillDisappear(_ animated: Bool) {
|
||||
|
@ -2498,4 +2498,86 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
final class SnapshotState {
|
||||
fileprivate let historySnapshotState: ChatHistoryListNode.SnapshotState
|
||||
let titleViewSnapshotState: ChatTitleView.SnapshotState?
|
||||
let avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState?
|
||||
let navigationButtonsSnapshotState: ChatHistoryNavigationButtons.SnapshotState
|
||||
let titleAccessoryPanelSnapshot: UIView?
|
||||
let navigationBarHeight: CGFloat
|
||||
|
||||
fileprivate init(
|
||||
historySnapshotState: ChatHistoryListNode.SnapshotState,
|
||||
titleViewSnapshotState: ChatTitleView.SnapshotState?,
|
||||
avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState?,
|
||||
navigationButtonsSnapshotState: ChatHistoryNavigationButtons.SnapshotState,
|
||||
titleAccessoryPanelSnapshot: UIView?,
|
||||
navigationBarHeight: CGFloat
|
||||
) {
|
||||
self.historySnapshotState = historySnapshotState
|
||||
self.titleViewSnapshotState = titleViewSnapshotState
|
||||
self.avatarSnapshotState = avatarSnapshotState
|
||||
self.navigationButtonsSnapshotState = navigationButtonsSnapshotState
|
||||
self.titleAccessoryPanelSnapshot = titleAccessoryPanelSnapshot
|
||||
self.navigationBarHeight = navigationBarHeight
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSnapshotState(
|
||||
titleViewSnapshotState: ChatTitleView.SnapshotState?,
|
||||
avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState?
|
||||
) -> SnapshotState {
|
||||
var titleAccessoryPanelSnapshot: UIView?
|
||||
if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let snapshot = titleAccessoryPanelNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshot.frame = titleAccessoryPanelNode.frame
|
||||
titleAccessoryPanelSnapshot = snapshot
|
||||
}
|
||||
return SnapshotState(
|
||||
historySnapshotState: self.historyNode.prepareSnapshotState(),
|
||||
titleViewSnapshotState: titleViewSnapshotState,
|
||||
avatarSnapshotState: avatarSnapshotState,
|
||||
navigationButtonsSnapshotState: self.navigateButtons.prepareSnapshotState(),
|
||||
titleAccessoryPanelSnapshot: titleAccessoryPanelSnapshot,
|
||||
navigationBarHeight: self.navigationBar?.backgroundNode.bounds.height ?? 0.0
|
||||
)
|
||||
}
|
||||
|
||||
func animateFromSnapshot(_ snapshotState: SnapshotState) {
|
||||
self.historyNode.animateFromSnapshot(snapshotState.historySnapshotState)
|
||||
self.navigateButtons.animateFromSnapshot(snapshotState.navigationButtonsSnapshotState)
|
||||
|
||||
if let titleAccessoryPanelSnapshot = snapshotState.titleAccessoryPanelSnapshot {
|
||||
self.titleAccessoryPanelContainer.view.addSubview(titleAccessoryPanelSnapshot)
|
||||
if let _ = self.titleAccessoryPanelNode {
|
||||
titleAccessoryPanelSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak titleAccessoryPanelSnapshot] _ in
|
||||
titleAccessoryPanelSnapshot?.removeFromSuperview()
|
||||
})
|
||||
titleAccessoryPanelSnapshot.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -10.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
} else {
|
||||
titleAccessoryPanelSnapshot.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -titleAccessoryPanelSnapshot.bounds.height), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak titleAccessoryPanelSnapshot] _ in
|
||||
titleAccessoryPanelSnapshot?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let titleAccessoryPanelNode = self.titleAccessoryPanelNode {
|
||||
if let _ = snapshotState.titleAccessoryPanelSnapshot {
|
||||
titleAccessoryPanelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
|
||||
titleAccessoryPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: true)
|
||||
} else {
|
||||
titleAccessoryPanelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -titleAccessoryPanelNode.bounds.height), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
if let navigationBar = self.navigationBar {
|
||||
let currentFrame = navigationBar.backgroundNode.frame
|
||||
var previousFrame = currentFrame
|
||||
previousFrame.size.height = snapshotState.navigationBarHeight
|
||||
if previousFrame != currentFrame {
|
||||
navigationBar.backgroundNode.update(size: previousFrame.size, transition: .immediate)
|
||||
navigationBar.backgroundNode.update(size: currentFrame.size, transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import ListMessageItem
|
||||
import AccountContext
|
||||
import ChatInterfaceState
|
||||
import ChatListUI
|
||||
import ComponentFlow
|
||||
|
||||
extension ChatReplyThreadMessage {
|
||||
var effectiveTopId: MessageId {
|
||||
@ -546,6 +547,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
let topVisibleMessageRange = ValuePromise<ChatTopVisibleMessageRange?>(nil, ignoreRepeated: true)
|
||||
|
||||
var isSelectionGestureEnabled = true
|
||||
|
||||
private var overscrollView: ComponentHostView<Empty>?
|
||||
var nextChannelToRead: EnginePeer?
|
||||
var offerNextChannelToRead: Bool = false
|
||||
private var currentOverscrollExpandProgress: CGFloat = 0.0
|
||||
private var feedback: HapticFeedback?
|
||||
var openNextChannelToRead: ((EnginePeer) -> Void)?
|
||||
|
||||
private let clientId: Atomic<Int32>
|
||||
|
||||
@ -577,6 +585,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
nextClientId += 1
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = false
|
||||
|
||||
self.accessibilityPageScrolledString = { [weak self] row, count in
|
||||
if let strongSelf = self {
|
||||
@ -617,7 +627,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
|
||||
self.preloadPages = false
|
||||
self.preloadPages = true
|
||||
switch self.mode {
|
||||
case .bubbles:
|
||||
self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
||||
@ -1115,11 +1125,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
if strongSelf.tagMask == nil {
|
||||
var atBottom = false
|
||||
var offsetFromBottom: CGFloat?
|
||||
switch offset {
|
||||
case let .known(offsetValue):
|
||||
if offsetValue.isLessThanOrEqualTo(0.0) {
|
||||
atBottom = true
|
||||
offsetFromBottom = offsetValue
|
||||
}
|
||||
//print("offsetValue: \(offsetValue)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -1130,6 +1143,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
strongSelf.isScrollAtBottomPositionUpdated?()
|
||||
}
|
||||
|
||||
strongSelf.maybeUpdateOverscrollAction(offset: offsetFromBottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1150,10 +1165,22 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
self?.isInteractivelyScrollingPromise.set(true)
|
||||
self?.beganDragging?()
|
||||
}
|
||||
|
||||
self.endedInteractiveDragging = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let channel = strongSelf.nextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 {
|
||||
strongSelf.openNextChannelToRead?(channel)
|
||||
}
|
||||
}
|
||||
|
||||
self.didEndScrolling = { [weak self] in
|
||||
self?.isInteractivelyScrollingValue = false
|
||||
self?.isInteractivelyScrollingPromise.set(false)
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isInteractivelyScrollingValue = false
|
||||
strongSelf.isInteractivelyScrollingPromise.set(false)
|
||||
}
|
||||
|
||||
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
||||
@ -1177,6 +1204,64 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) {
|
||||
self.loadStateUpdated = f
|
||||
}
|
||||
|
||||
private func maybeUpdateOverscrollAction(offset: CGFloat?) {
|
||||
if let offset = offset, offset < 0.0, self.offerNextChannelToRead {
|
||||
let overscrollView: ComponentHostView<Empty>
|
||||
if let current = self.overscrollView {
|
||||
overscrollView = current
|
||||
} else {
|
||||
overscrollView = ComponentHostView<Empty>()
|
||||
overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
self.overscrollView = overscrollView
|
||||
self.view.addSubview(overscrollView)
|
||||
}
|
||||
|
||||
let expandProgress: CGFloat = min(1.0, max(-offset, 0.0) / 110.0)
|
||||
|
||||
let text: String
|
||||
if self.nextChannelToRead != nil {
|
||||
if expandProgress >= 0.99 {
|
||||
//TODO:localize
|
||||
text = "Release to go to the next unread channel"
|
||||
} else {
|
||||
text = "Swipe up to go to the next unread channel"
|
||||
}
|
||||
|
||||
let previousType = self.currentOverscrollExpandProgress >= 0.99
|
||||
let currentType = expandProgress >= 0.99
|
||||
|
||||
if previousType != currentType {
|
||||
if self.feedback == nil {
|
||||
self.feedback = HapticFeedback()
|
||||
}
|
||||
self.feedback?.tap()
|
||||
}
|
||||
|
||||
self.currentOverscrollExpandProgress = expandProgress
|
||||
} else {
|
||||
text = "You have no unread channels"
|
||||
}
|
||||
|
||||
let overscrollSize = overscrollView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ChatOverscrollControl(
|
||||
text: text,
|
||||
backgroundColor: selectDateFillStaticColor(theme: self.currentPresentationData.theme.theme, wallpaper: self.currentPresentationData.theme.wallpaper),
|
||||
foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper),
|
||||
peer: self.nextChannelToRead,
|
||||
context: self.context,
|
||||
expandProgress: expandProgress
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: self.bounds.width, height: 200.0)
|
||||
)
|
||||
overscrollView.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - overscrollSize.width) / 2.0), y: -offset + self.insets.top - overscrollSize.height - 10.0), size: overscrollSize)
|
||||
} else if let overscrollView = self.overscrollView {
|
||||
self.overscrollView = nil
|
||||
overscrollView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func refreshPollActionsForVisibleMessages() {
|
||||
let _ = self.clientId.swap(nextClientId)
|
||||
@ -2324,4 +2409,76 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
|
||||
var animationCorrelationMessageFound: ((ChatMessageItemView, Int64?) -> Void)?
|
||||
|
||||
final class SnapshotState {
|
||||
fileprivate let snapshotTopInset: CGFloat
|
||||
fileprivate let snapshotBottomInset: CGFloat
|
||||
fileprivate let snapshotView: UIView
|
||||
|
||||
fileprivate init(
|
||||
snapshotTopInset: CGFloat,
|
||||
snapshotBottomInset: CGFloat,
|
||||
snapshotView: UIView
|
||||
) {
|
||||
self.snapshotTopInset = snapshotTopInset
|
||||
self.snapshotBottomInset = snapshotBottomInset
|
||||
self.snapshotView = snapshotView
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSnapshotState() -> SnapshotState {
|
||||
var snapshotTopInset: CGFloat = 0.0
|
||||
var snapshotBottomInset: CGFloat = 0.0
|
||||
self.forEachItemNode { itemNode in
|
||||
let topOverflow = itemNode.frame.maxY - self.bounds.height
|
||||
snapshotTopInset = max(snapshotTopInset, topOverflow)
|
||||
|
||||
if itemNode.frame.minY < 0.0 {
|
||||
snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY)
|
||||
}
|
||||
}
|
||||
let snapshotView = self.view.snapshotView(afterScreenUpdates: false)!
|
||||
|
||||
let currentSnapshotView = self.view.snapshotView(afterScreenUpdates: false)!
|
||||
currentSnapshotView.frame = self.view.bounds
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
sublayer.isHidden = true
|
||||
}
|
||||
}
|
||||
self.view.addSubview(currentSnapshotView)
|
||||
|
||||
return SnapshotState(
|
||||
snapshotTopInset: snapshotTopInset,
|
||||
snapshotBottomInset: snapshotBottomInset,
|
||||
snapshotView: snapshotView
|
||||
)
|
||||
}
|
||||
|
||||
func animateFromSnapshot(_ snapshotState: SnapshotState) {
|
||||
var snapshotTopInset: CGFloat = 0.0
|
||||
var snapshotBottomInset: CGFloat = 0.0
|
||||
self.forEachItemNode { itemNode in
|
||||
let topOverflow = itemNode.frame.maxY - self.bounds.height
|
||||
snapshotTopInset = max(snapshotTopInset, topOverflow)
|
||||
|
||||
if itemNode.frame.minY < 0.0 {
|
||||
snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY)
|
||||
}
|
||||
}
|
||||
|
||||
let snapshotParentView = UIView()
|
||||
snapshotParentView.addSubview(snapshotState.snapshotView)
|
||||
snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
||||
snapshotParentView.frame = self.view.frame
|
||||
|
||||
snapshotState.snapshotView.frame = snapshotParentView.bounds
|
||||
self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view)
|
||||
|
||||
snapshotParentView.layer.animatePosition(from: CGPoint(x: 0.0, y: 0.0), to: CGPoint(x: 0.0, y: -self.view.bounds.height - snapshotState.snapshotBottomInset - snapshotTopInset), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak snapshotParentView] _ in
|
||||
snapshotParentView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
self.view.layer.animatePosition(from: CGPoint(x: 0.0, y: self.view.bounds.height + snapshotTopInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
|
||||
}
|
||||
}
|
||||
|
@ -163,4 +163,34 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SnapshotState {
|
||||
fileprivate let downButtonSnapshotView: UIView?
|
||||
|
||||
fileprivate init(
|
||||
downButtonSnapshotView: UIView?
|
||||
) {
|
||||
self.downButtonSnapshotView = downButtonSnapshotView
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSnapshotState() -> SnapshotState {
|
||||
var downButtonSnapshotView: UIView?
|
||||
if !self.downButton.isHidden {
|
||||
downButtonSnapshotView = self.downButton.view.snapshotView(afterScreenUpdates: false)!
|
||||
}
|
||||
return SnapshotState(
|
||||
downButtonSnapshotView: downButtonSnapshotView
|
||||
)
|
||||
}
|
||||
|
||||
func animateFromSnapshot(_ snapshotState: SnapshotState) {
|
||||
if self.downButton.isHidden != (snapshotState.downButtonSnapshotView == nil) {
|
||||
if self.downButton.isHidden {
|
||||
} else {
|
||||
self.downButton.layer.animateAlpha(from: 0.0, to: self.downButton.alpha, duration: 0.3)
|
||||
self.downButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
518
submodules/TelegramUI/Sources/ChatOverscrollControl.swift
Normal file
518
submodules/TelegramUI/Sources/ChatOverscrollControl.swift
Normal file
@ -0,0 +1,518 @@
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
import Display
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import AccountContext
|
||||
import AvatarNode
|
||||
|
||||
final class BlurredRoundedRectangle: Component {
|
||||
let color: UIColor
|
||||
|
||||
init(color: UIColor) {
|
||||
self.color = color
|
||||
}
|
||||
|
||||
static func ==(lhs: BlurredRoundedRectangle, rhs: BlurredRoundedRectangle) -> Bool {
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let background: NavigationBackgroundNode
|
||||
|
||||
init() {
|
||||
self.background = NavigationBackgroundNode(color: .clear)
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.background.view)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: BlurredRoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
self.background.updateColor(color: component.color, transition: .immediate)
|
||||
self.background.update(size: availableSize, cornerRadius: min(availableSize.width, availableSize.height) / 2.0, transition: .immediate)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class RadialProgressComponent: Component {
|
||||
let color: UIColor
|
||||
let lineWidth: CGFloat
|
||||
let value: CGFloat
|
||||
|
||||
init(
|
||||
color: UIColor,
|
||||
lineWidth: CGFloat,
|
||||
value: CGFloat
|
||||
) {
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.value = value
|
||||
}
|
||||
|
||||
static func ==(lhs: RadialProgressComponent, rhs: RadialProgressComponent) -> Bool {
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
if lhs.lineWidth != rhs.lineWidth {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: RadialProgressComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
func draw(context: CGContext) {
|
||||
let diameter = availableSize.width
|
||||
|
||||
context.saveGState()
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(component.color.cgColor)
|
||||
context.setStrokeColor(component.color.cgColor)
|
||||
|
||||
var progress: CGFloat
|
||||
var startAngle: CGFloat
|
||||
var endAngle: CGFloat
|
||||
|
||||
let value = component.value
|
||||
|
||||
progress = value
|
||||
startAngle = -CGFloat.pi / 2.0
|
||||
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
||||
|
||||
if progress > 1.0 {
|
||||
progress = 2.0 - progress
|
||||
let tmp = startAngle
|
||||
startAngle = endAngle
|
||||
endAngle = tmp
|
||||
}
|
||||
progress = min(1.0, progress)
|
||||
|
||||
let lineWidth: CGFloat = component.lineWidth
|
||||
|
||||
let pathDiameter: CGFloat
|
||||
|
||||
pathDiameter = diameter - lineWidth
|
||||
|
||||
var angle: Double = 0.0
|
||||
angle *= 4.0
|
||||
|
||||
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
|
||||
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
|
||||
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
|
||||
|
||||
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
||||
path.lineWidth = lineWidth
|
||||
path.lineCapStyle = .round
|
||||
path.stroke()
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||
let image = renderer.image { context in
|
||||
UIGraphicsPushContext(context.cgContext)
|
||||
draw(context: context.cgContext)
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
self.layer.contents = image.cgImage
|
||||
} else {
|
||||
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
|
||||
draw(context: UIGraphicsGetCurrentContext()!)
|
||||
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class CheckComponent: Component {
|
||||
let color: UIColor
|
||||
let lineWidth: CGFloat
|
||||
let value: CGFloat
|
||||
|
||||
init(
|
||||
color: UIColor,
|
||||
lineWidth: CGFloat,
|
||||
value: CGFloat
|
||||
) {
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.value = value
|
||||
}
|
||||
|
||||
static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool {
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
if lhs.lineWidth != rhs.lineWidth {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
func draw(context: CGContext) {
|
||||
let size = availableSize
|
||||
|
||||
let diameter = size.width
|
||||
|
||||
let factor = diameter / 50.0
|
||||
|
||||
context.saveGState()
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(component.color.cgColor)
|
||||
context.setStrokeColor(component.color.cgColor)
|
||||
|
||||
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
|
||||
|
||||
let lineWidth = component.lineWidth
|
||||
|
||||
context.setLineWidth(max(1.7, lineWidth * factor))
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
context.setMiterLimit(10.0)
|
||||
|
||||
let progress = component.value
|
||||
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
|
||||
|
||||
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
|
||||
var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor)
|
||||
var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor)
|
||||
|
||||
if diameter < 36.0 {
|
||||
s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor)
|
||||
p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor)
|
||||
p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor)
|
||||
}
|
||||
|
||||
if !firstSegment.isZero {
|
||||
if firstSegment < 1.0 {
|
||||
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
|
||||
context.addLine(to: s)
|
||||
} else {
|
||||
let secondSegment = (progress - 0.33) * 1.5
|
||||
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
|
||||
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
|
||||
context.addLine(to: s)
|
||||
}
|
||||
}
|
||||
context.strokePath()
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||
let image = renderer.image { context in
|
||||
UIGraphicsPushContext(context.cgContext)
|
||||
draw(context: context.cgContext)
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
self.layer.contents = image.cgImage
|
||||
} else {
|
||||
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
|
||||
draw(context: UIGraphicsGetCurrentContext()!)
|
||||
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class AvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let peer: EnginePeer
|
||||
|
||||
init(context: AccountContext, peer: EnginePeer) {
|
||||
self.context = context
|
||||
self.peer = peer
|
||||
}
|
||||
|
||||
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
init() {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.avatarNode.view)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: AvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, peer: component.peer._asPeer(), synchronousLoad: true)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatOverscrollControl: CombinedComponent {
|
||||
let text: String
|
||||
let backgroundColor: UIColor
|
||||
let foregroundColor: UIColor
|
||||
let peer: EnginePeer?
|
||||
let context: AccountContext
|
||||
let expandProgress: CGFloat
|
||||
|
||||
init(
|
||||
text: String,
|
||||
backgroundColor: UIColor,
|
||||
foregroundColor: UIColor,
|
||||
peer: EnginePeer?,
|
||||
context: AccountContext,
|
||||
expandProgress: CGFloat
|
||||
) {
|
||||
self.text = text
|
||||
self.backgroundColor = backgroundColor
|
||||
self.foregroundColor = foregroundColor
|
||||
self.peer = peer
|
||||
self.context = context
|
||||
self.expandProgress = expandProgress
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
||||
return false
|
||||
}
|
||||
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.expandProgress != rhs.expandProgress {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let avatarBackground = Child(BlurredRoundedRectangle.self)
|
||||
let avatarExpandProgress = Child(RadialProgressComponent.self)
|
||||
let avatarCheck = Child(CheckComponent.self)
|
||||
let avatar = Child(AvatarComponent.self)
|
||||
let textBackground = Child(BlurredRoundedRectangle.self)
|
||||
let text = Child(Text.self)
|
||||
|
||||
return { context in
|
||||
let text = text.update(
|
||||
component: Text(
|
||||
text: context.component.text,
|
||||
font: Font.regular(12.0),
|
||||
color: context.component.foregroundColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 100.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let textHorizontalPadding: CGFloat = 6.0
|
||||
let textVerticalPadding: CGFloat = 2.0
|
||||
let avatarSize: CGFloat = 48.0
|
||||
let avatarPadding: CGFloat = 8.0
|
||||
let avatarTextSpacing: CGFloat = 8.0
|
||||
let avatarProgressPadding: CGFloat = 2.5
|
||||
|
||||
let avatarBackgroundSize: CGFloat = context.component.peer != nil ? (avatarSize + avatarPadding * 2.0) : avatarSize
|
||||
|
||||
let avatarBackground = avatarBackground.update(
|
||||
component: BlurredRoundedRectangle(
|
||||
color: context.component.backgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: avatarBackgroundSize, height: avatarBackgroundSize),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let avatarCheck = Condition(context.component.peer == nil, { () -> _UpdatedChildComponent in
|
||||
let avatarCheckSize = avatarBackgroundSize + 2.0
|
||||
|
||||
return avatarCheck.update(
|
||||
component: CheckComponent(
|
||||
color: context.component.foregroundColor,
|
||||
lineWidth: 2.5,
|
||||
value: 1.0
|
||||
),
|
||||
availableSize: CGSize(width: avatarCheckSize, height: avatarCheckSize),
|
||||
transition: context.transition
|
||||
)
|
||||
})
|
||||
|
||||
let avatarExpandProgress = avatarExpandProgress.update(
|
||||
component: RadialProgressComponent(
|
||||
color: context.component.foregroundColor,
|
||||
lineWidth: 2.5,
|
||||
value: context.component.peer == nil ? 0.0 : context.component.expandProgress
|
||||
),
|
||||
availableSize: CGSize(width: avatarBackground.size.width - avatarProgressPadding * 2.0, height: avatarBackground.size.height - avatarProgressPadding * 2.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let textBackground = textBackground.update(
|
||||
component: BlurredRoundedRectangle(
|
||||
color: context.component.backgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: text.size.width + textHorizontalPadding * 2.0, height: text.size.height + textVerticalPadding * 2.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let size = CGSize(width: context.availableSize.width, height: avatarBackground.size.height + avatarTextSpacing + textBackground.size.height)
|
||||
|
||||
let avatarBackgroundFrame = avatarBackground.size.topCentered(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let avatar = context.component.peer.flatMap { peer in
|
||||
avatar.update(
|
||||
component: AvatarComponent(
|
||||
context: context.component.context,
|
||||
peer: peer
|
||||
),
|
||||
availableSize: CGSize(width: avatarSize, height: avatarSize),
|
||||
transition: context.transition
|
||||
)
|
||||
}
|
||||
|
||||
context.add(avatarBackground
|
||||
.position(CGPoint(
|
||||
x: avatarBackgroundFrame.midX,
|
||||
y: avatarBackgroundFrame.midY
|
||||
))
|
||||
)
|
||||
|
||||
if let avatarCheck = avatarCheck {
|
||||
context.add(avatarCheck
|
||||
.position(CGPoint(
|
||||
x: avatarBackgroundFrame.midX,
|
||||
y: avatarBackgroundFrame.midY
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
context.add(avatarExpandProgress
|
||||
.position(CGPoint(
|
||||
x: avatarBackgroundFrame.midX,
|
||||
y: avatarBackgroundFrame.midY
|
||||
))
|
||||
)
|
||||
|
||||
if let avatar = avatar {
|
||||
context.add(avatar
|
||||
.position(CGPoint(
|
||||
x: avatarBackgroundFrame.midX,
|
||||
y: avatarBackgroundFrame.midY
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
let textBackgroundFrame = textBackground.size.bottomCentered(in: CGRect(origin: CGPoint(), size: size))
|
||||
context.add(textBackground
|
||||
.position(CGPoint(
|
||||
x: textBackgroundFrame.midX,
|
||||
y: textBackgroundFrame.midY
|
||||
))
|
||||
)
|
||||
|
||||
let textFrame = text.size.centered(in: textBackgroundFrame)
|
||||
context.add(text
|
||||
.position(CGPoint(
|
||||
x: textFrame.midX,
|
||||
y: textFrame.midY
|
||||
))
|
||||
)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
@ -736,4 +736,33 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
final class SnapshotState {
|
||||
fileprivate let snapshotView: UIView
|
||||
|
||||
fileprivate init(snapshotView: UIView) {
|
||||
self.snapshotView = snapshotView
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSnapshotState() -> SnapshotState {
|
||||
let snapshotView = self.snapshotView(afterScreenUpdates: false)!
|
||||
return SnapshotState(
|
||||
snapshotView: snapshotView
|
||||
)
|
||||
}
|
||||
|
||||
func animateFromSnapshot(_ snapshotState: SnapshotState) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.layer.animatePosition(from: CGPoint(x: 0.0, y: 20.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
|
||||
|
||||
snapshotState.snapshotView.frame = self.frame
|
||||
self.superview?.insertSubview(snapshotState.snapshotView, belowSubview: self)
|
||||
|
||||
let snapshotView = snapshotState.snapshotView
|
||||
snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -20.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import ChatInterfaceState
|
||||
private var telegramUIDeclaredEncodables: Void = {
|
||||
declareEncodable(InAppNotificationSettings.self, f: { InAppNotificationSettings(decoder: $0) })
|
||||
declareEncodable(ChatInterfaceState.self, f: { ChatInterfaceState(decoder: $0) })
|
||||
declareEncodable(ChatEmbeddedInterfaceState.self, f: { ChatEmbeddedInterfaceState(decoder: $0) })
|
||||
declareEncodable(VideoLibraryMediaResource.self, f: { VideoLibraryMediaResource(decoder: $0) })
|
||||
declareEncodable(LocalFileVideoMediaResource.self, f: { LocalFileVideoMediaResource(decoder: $0) })
|
||||
declareEncodable(LocalFileGifMediaResource.self, f: { LocalFileGifMediaResource(decoder: $0) })
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "7.9",
|
||||
"bazel": "4.0.0",
|
||||
"xcode": "12.4"
|
||||
"xcode": "12.5.1"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user