Skip to content

Commit 276c165

Browse files
KM-13582 Move additional UI elements from PIA VPN to PIAUIKit and PIASwiftUI
1 parent 944c018 commit 276c165

31 files changed

Lines changed: 244 additions & 230 deletions

LocalPackages/PIALibrary/Sources/PIALibrary/Util/Data+Compression.swift

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,18 @@ import zlib
2525

2626
private let compressionBlockSize = 16384
2727

28+
enum CompressionError: Error {
29+
case initialization
30+
case processing
31+
case memory
32+
case underlying(Error)
33+
}
34+
2835
extension Data {
2936
/// Compresses the data using zlib deflate algorithm.
30-
/// - Returns: The compressed data, or nil if compression fails.
31-
func deflated() -> Data? {
37+
/// - Returns: The compressed data.
38+
/// - Throws: A `CompressionError` if compression fails.
39+
func deflated() throws(CompressionError) -> Data {
3240
guard !isEmpty else {
3341
return self
3442
}
@@ -40,7 +48,7 @@ extension Data {
4048
stream.total_out = 0
4149

4250
guard deflateInit2_(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, MAX_WBITS, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size)) == Z_OK else {
43-
return nil
51+
throw CompressionError.initialization
4452
}
4553

4654
defer {
@@ -50,43 +58,46 @@ extension Data {
5058
var compressed = Data(count: compressionBlockSize)
5159
var compressedSize = compressed.count
5260

53-
let result: Data? = withUnsafeBytes { (inputBytes: UnsafeRawBufferPointer) -> Data? in
54-
guard let inputBaseAddress = inputBytes.bindMemory(to: UInt8.self).baseAddress else {
55-
return nil
56-
}
57-
stream.next_in = UnsafeMutablePointer<UInt8>(mutating: inputBaseAddress)
58-
stream.avail_in = uInt(count)
59-
60-
repeat {
61-
if Int(stream.total_out) >= compressedSize {
62-
compressedSize += compressionBlockSize
63-
compressed.count = compressedSize
61+
do {
62+
return try withUnsafeBytes { (inputBytes: UnsafeRawBufferPointer) -> Data in
63+
guard let inputBaseAddress = inputBytes.bindMemory(to: UInt8.self).baseAddress else {
64+
throw CompressionError.memory
6465
}
66+
stream.next_in = UnsafeMutablePointer<UInt8>(mutating: inputBaseAddress)
67+
stream.avail_in = uInt(count)
6568

66-
let status = compressed.withUnsafeMutableBytes { (outputBytes: UnsafeMutableRawBufferPointer) -> Int32 in
67-
guard let outputBaseAddress = outputBytes.bindMemory(to: UInt8.self).baseAddress else {
68-
return Z_STREAM_ERROR
69+
repeat {
70+
if Int(stream.total_out) >= compressedSize {
71+
compressedSize += compressionBlockSize
72+
compressed.count = compressedSize
6973
}
70-
stream.next_out = outputBaseAddress.advanced(by: Int(stream.total_out))
71-
stream.avail_out = uInt(compressedSize - Int(stream.total_out))
72-
return deflate(&stream, Z_FINISH)
73-
}
7474

75-
if status < 0 {
76-
return nil
77-
}
78-
} while stream.avail_out == 0
75+
let status = compressed.withUnsafeMutableBytes { (outputBytes: UnsafeMutableRawBufferPointer) -> Int32 in
76+
guard let outputBaseAddress = outputBytes.bindMemory(to: UInt8.self).baseAddress else {
77+
return Z_STREAM_ERROR
78+
}
79+
stream.next_out = outputBaseAddress.advanced(by: Int(stream.total_out))
80+
stream.avail_out = uInt(compressedSize - Int(stream.total_out))
81+
return deflate(&stream, Z_FINISH)
82+
}
7983

80-
compressed.count = Int(stream.total_out)
81-
return compressed
82-
}
84+
if status < 0 {
85+
throw CompressionError.processing
86+
}
87+
} while stream.avail_out == 0
8388

84-
return result
89+
compressed.count = Int(stream.total_out)
90+
return compressed
91+
}
92+
} catch {
93+
throw .underlying(error)
94+
}
8595
}
8696

8797
/// Decompresses the data using zlib inflate algorithm.
88-
/// - Returns: The decompressed data, or nil if decompression fails.
89-
func inflated() -> Data? {
98+
/// - Returns: The decompressed data.
99+
/// - Throws: A `CompressionError` if decompression fails.
100+
func inflated() throws -> Data {
90101
guard !isEmpty else {
91102
return self
92103
}
@@ -100,7 +111,7 @@ extension Data {
100111
stream.total_out = 0
101112

102113
guard inflateInit2_(&stream, MAX_WBITS, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size)) == Z_OK else {
103-
return nil
114+
throw CompressionError.initialization
104115
}
105116

106117
defer {
@@ -111,9 +122,9 @@ extension Data {
111122
var decompressedSize = decompressed.count
112123
var done = false
113124

114-
let result: Data? = withUnsafeBytes { (inputBytes: UnsafeRawBufferPointer) -> Data? in
125+
return try withUnsafeBytes { (inputBytes: UnsafeRawBufferPointer) -> Data in
115126
guard let inputBaseAddress = inputBytes.bindMemory(to: UInt8.self).baseAddress else {
116-
return nil
127+
throw CompressionError.memory
117128
}
118129
stream.next_in = UnsafeMutablePointer<UInt8>(mutating: inputBaseAddress)
119130
stream.avail_in = uInt(count)
@@ -136,22 +147,17 @@ extension Data {
136147
if status == Z_STREAM_END {
137148
done = true
138149
} else if status != Z_OK {
139-
return nil
150+
throw CompressionError.processing
140151
}
141152
}
142153

143-
guard inflateEnd(&stream) == Z_OK else {
144-
return nil
145-
}
146-
147154
guard done else {
148-
return nil
155+
throw CompressionError.processing
149156
}
150157

151158
decompressed.count = Int(stream.total_out)
152159
return decompressed
153160
}
154-
155-
return result
156161
}
157162
}
163+

LocalPackages/PIALibrary/Tests/PIALibraryTests/DataCompressionTests.swift

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -28,78 +28,59 @@ import Foundation
2828
struct DataCompressionTests {
2929

3030
@Test("Compress and decompress simple text")
31-
func compressAndDecompressSimpleText() {
31+
func compressAndDecompressSimpleText() throws {
3232
let original = "This is a test"
3333
let originalData = original.data(using: .utf8)!
34+
let compressed = try originalData.deflated()
3435

35-
let compressed = originalData.deflated()
36-
#expect(compressed != nil, "Compression should succeed")
37-
// Note: Small strings may not compress smaller due to compression overhead
38-
39-
let decompressed = compressed!.inflated()
40-
#expect(decompressed != nil, "Decompression should succeed")
41-
42-
let decompressedString = String(data: decompressed!, encoding: .utf8)
36+
let decompressed = try compressed.inflated()
37+
let decompressedString = String(data: decompressed, encoding: .utf8)
4338
#expect(decompressedString == original, "Decompressed string should match original")
4439
}
4540

4641
@Test("Compress and decompress longer text")
47-
func compressAndDecompressLongerText() {
48-
let original = String(repeating: "The quick brown fox jumps over the lazy dog. ", count: 100)
42+
func compressAndDecompressLongerText() throws {
43+
let original = String(repeating: "The quick brown fox jumps over the lazy dog.", count: 100)
4944
let originalData = original.data(using: .utf8)!
45+
let compressed = try originalData.deflated()
46+
#expect(compressed.count < originalData.count, "Compressed data should be smaller for repetitive text")
5047

51-
let compressed = originalData.deflated()
52-
#expect(compressed != nil, "Compression should succeed")
53-
#expect(compressed!.count < originalData.count, "Compressed data should be smaller for repetitive text")
54-
55-
let decompressed = compressed!.inflated()
56-
#expect(decompressed != nil, "Decompression should succeed")
57-
58-
let decompressedString = String(data: decompressed!, encoding: .utf8)
48+
let decompressed = try compressed.inflated()
49+
let decompressedString = String(data: decompressed, encoding: .utf8)
5950
#expect(decompressedString == original, "Decompressed string should match original")
6051
}
6152

6253
@Test("Handle empty data")
63-
func handleEmptyData() {
54+
func handleEmptyData() throws {
6455
let emptyData = Data()
56+
let compressed = try emptyData.deflated()
57+
#expect(compressed.isEmpty, "Compressed empty data should be empty")
6558

66-
let compressed = emptyData.deflated()
67-
#expect(compressed != nil, "Compression of empty data should return empty data")
68-
#expect(compressed!.isEmpty, "Compressed empty data should be empty")
69-
70-
let decompressed = emptyData.inflated()
71-
#expect(decompressed != nil, "Decompression of empty data should return empty data")
72-
#expect(decompressed!.isEmpty, "Decompressed empty data should be empty")
59+
let decompressed = try emptyData.inflated()
60+
#expect(decompressed.isEmpty, "Decompressed empty data should be empty")
7361
}
7462

7563
@Test("Compress binary data")
76-
func compressBinaryData() {
64+
func compressBinaryData() throws {
7765
let binaryData = Data([0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC])
66+
let compressed = try binaryData.deflated()
7867

79-
let compressed = binaryData.deflated()
80-
#expect(compressed != nil, "Compression of binary data should succeed")
81-
82-
let decompressed = compressed!.inflated()
83-
#expect(decompressed != nil, "Decompression should succeed")
68+
let decompressed = try compressed.inflated()
8469
#expect(decompressed == binaryData, "Decompressed binary data should match original")
8570
}
8671

8772
@Test("Handle large data")
88-
func handleLargeData() {
89-
// Create a large data set (1MB)
90-
let largeData = Data(repeating: 0x42, count: 1024 * 1024)
91-
92-
let compressed = largeData.deflated()
93-
#expect(compressed != nil, "Compression of large data should succeed")
94-
#expect(compressed!.count < largeData.count, "Compressed large data should be much smaller")
73+
func handleLargeData() throws {
74+
let largeData = Data(repeating: 0x42, count: 1024 * 1024) // (1MB)
75+
let compressed = try largeData.deflated()
76+
#expect(compressed.count < largeData.count, "Compressed large data should be much smaller")
9577

96-
let decompressed = compressed!.inflated()
97-
#expect(decompressed != nil, "Decompression should succeed")
78+
let decompressed = try compressed.inflated()
9879
#expect(decompressed == largeData, "Decompressed large data should match original")
9980
}
10081

10182
@Test("Round-trip compression maintains data integrity")
102-
func roundTripCompressionIntegrity() {
83+
func roundTripCompressionIntegrity() throws {
10384
let testStrings = [
10485
"Hello, World!",
10586
"1234567890",
@@ -110,10 +91,10 @@ struct DataCompressionTests {
11091

11192
for testString in testStrings {
11293
let originalData = testString.data(using: .utf8)!
113-
let compressed = originalData.deflated()
114-
let decompressed = compressed?.inflated()
115-
let result = String(data: decompressed!, encoding: .utf8)
94+
let compressed = try originalData.deflated()
11695

96+
let decompressed = try compressed.inflated()
97+
let result = String(data: decompressed, encoding: .utf8)
11798
#expect(result == testString, "Round-trip should preserve: \(testString)")
11899
}
119100
}

LocalPackages/PIAUI/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# PIAUI
2+
3+
Shared UI components and design system for PIA VPN iOS/tvOS applications.
4+
5+
## Libraries
6+
7+
### PIADesignSystem
8+
Core design system containing colors, assets, and visual design tokens.
9+
10+
### PIAUIKit
11+
UIKit-based reusable UI components including custom controls, views, and extensions.
12+
13+
### PIASwiftUI
14+
SwiftUI-based reusable UI components.
15+
16+
## Usage
17+
18+
Import the specific library you need in your Swift files:
19+
20+
```swift
21+
import PIADesignSystem
22+
import PIAUIKit
23+
import PIASwiftUI
24+
```

PIA VPN/UI/Loading/LoadingView.swift renamed to LocalPackages/PIAUI/Sources/PIASwiftUI/LoadingView.swift

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@
2323
import SwiftUI
2424
import UIKit
2525

26-
struct LoadingView: View {
26+
public struct LoadingView: View {
27+
private let image: Image
2728
@State private var isRotating = false
2829

29-
var body: some View {
30-
Image(asset: Asset.Ui.piaSpinner)
30+
public init(image: Image) {
31+
self.image = image
32+
}
33+
34+
public var body: some View {
35+
image
3136
.resizable()
3237
.aspectRatio(contentMode: .fit)
3338
.rotationEffect(.degrees(isRotating ? 360 : 0))
@@ -43,18 +48,14 @@ struct LoadingView: View {
4348
}
4449

4550
/// UIKit wrapper for LoadingView
46-
final class LoadingViewController: UIHostingController<LoadingView> {
47-
init() {
48-
super.init(rootView: LoadingView())
51+
public final class LoadingViewController: UIHostingController<LoadingView> {
52+
public init(image: Image) {
53+
super.init(rootView: LoadingView(image: image))
4954
view.backgroundColor = .clear
5055
}
5156

5257
@available(*, unavailable)
53-
required init?(coder aDecoder: NSCoder) {
58+
public required init?(coder aDecoder: NSCoder) {
5459
fatalError("init(coder:) has not been implemented")
5560
}
5661
}
57-
58-
#Preview {
59-
LoadingView()
60-
}

LocalPackages/PIAUI/Sources/PIASwiftUI/PIASwiftUI.swift

Lines changed: 0 additions & 3 deletions
This file was deleted.

LocalPackages/PIAUI/Sources/PIAUIKit/Autolayout/UIViewAutolayout.swift

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,31 @@
2323
import UIKit
2424

2525
public extension UIView {
26-
26+
2727
func addConstaintsToSuperview(leadingOffset: CGFloat, trailingOffset: CGFloat, topOffset: CGFloat, bottomOffset: CGFloat) {
28-
29-
guard superview != nil else {
30-
return
31-
}
32-
28+
guard let superview = superview else { return }
29+
3330
translatesAutoresizingMaskIntoConstraints = false
34-
35-
leadingAnchor.constraint(equalTo: superview!.leadingAnchor,
36-
constant: leadingOffset).isActive = true
37-
trailingAnchor.constraint(equalTo: superview!.trailingAnchor,
38-
constant: trailingOffset).isActive = true
39-
40-
topAnchor.constraint(equalTo: superview!.topAnchor,
41-
constant: topOffset).isActive = true
42-
bottomAnchor.constraint(equalTo: superview!.bottomAnchor,
43-
constant: bottomOffset).isActive = true
31+
32+
leadingAnchor.constraint(
33+
equalTo: superview.leadingAnchor,
34+
constant: leadingOffset
35+
).isActive = true
36+
37+
trailingAnchor.constraint(
38+
equalTo: superview.trailingAnchor,
39+
constant: trailingOffset
40+
).isActive = true
41+
42+
topAnchor.constraint(
43+
equalTo: superview.topAnchor,
44+
constant: topOffset
45+
).isActive = true
46+
47+
bottomAnchor.constraint(
48+
equalTo: superview.bottomAnchor,
49+
constant: bottomOffset
50+
).isActive = true
4451
}
4552

4653
}

0 commit comments

Comments
 (0)